From 805ef8d43ea29b5896f433fc9527c8d77bec2c9a Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 17 Feb 2015 20:32:54 +0100 Subject: [PATCH] - Fixed bug where a network that has frozen physics would resume redrawing after setData, setOptions etc. - Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default. - Added two new static smooth curves modes: curveCW and curve CCW. - Added request redraw for certain internal processes to reduce number of draw calls. --- HISTORY.md | 3 + dist/vis.js | 8481 ++++++++++--------- examples/network/06_groups.html | 10 +- examples/network/26_staticSmoothCurves.html | 21 +- examples/network/27_world_cup_network.html | 2 + lib/network/Edge.js | 35 +- lib/network/Groups.js | 69 +- lib/network/Network.js | 67 +- lib/network/Node.js | 2 +- lib/network/mixins/SelectionMixin.js | 4 +- 10 files changed, 4444 insertions(+), 4250 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a8a1984c..05dc363a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,9 @@ http://visjs.org - (added gradient coloring for lines, but set for release in 4.0 due to required refactoring of options) - Fixed bug where a network that has frozen physics would resume redrawing after setData, setOptions etc. +- Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default. +- Added two new static smooth curves modes: curveCW and curve CCW. +- Added request redraw for certain internal processes to reduce number of draw calls. ### Timeline diff --git a/dist/vis.js b/dist/vis.js index 22aae3fc..2a25c7c3 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -137,13 +137,13 @@ return /******/ (function(modules) { // webpackBootstrap // Network exports.Network = __webpack_require__(51); exports.network = { - Edge: __webpack_require__(52), + Edge: __webpack_require__(57), Groups: __webpack_require__(54), Images: __webpack_require__(55), - Node: __webpack_require__(53), - Popup: __webpack_require__(56), - dotparser: __webpack_require__(57), - gephiParser: __webpack_require__(58) + Node: __webpack_require__(56), + Popup: __webpack_require__(58), + dotparser: __webpack_require__(52), + gephiParser: __webpack_require__(53) }; // Deprecated since v3.0.0 @@ -22799,19 +22799,19 @@ return /******/ (function(modules) { // webpackBootstrap var hammerUtil = __webpack_require__(22); var DataSet = __webpack_require__(7); var DataView = __webpack_require__(9); - var dotparser = __webpack_require__(57); - var gephiParser = __webpack_require__(58); + var dotparser = __webpack_require__(52); + var gephiParser = __webpack_require__(53); var Groups = __webpack_require__(54); var Images = __webpack_require__(55); - var Node = __webpack_require__(53); - var Edge = __webpack_require__(52); - var Popup = __webpack_require__(56); + var Node = __webpack_require__(56); + var Edge = __webpack_require__(57); + var Popup = __webpack_require__(58); var MixinLoader = __webpack_require__(59); var Activator = __webpack_require__(36); - var locales = __webpack_require__(60); + var locales = __webpack_require__(70); // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(61); + __webpack_require__(71); /** * @constructor Network @@ -23030,7 +23030,8 @@ return /******/ (function(modules) { // webpackBootstrap hideNodesOnDrag: false, width : '100%', height : '100%', - selectable: true + selectable: true, + useDefaultGroups: true }; this.constants = util.extend({}, this.defaultOptions); this.pixelRatio = 1; @@ -23052,13 +23053,14 @@ return /******/ (function(modules) { // webpackBootstrap this.lockedOnNodeId = null; this.lockedOnNodeOffset = null; this.touchTime = 0; + this.redrawRequested = false; // Node variables var network = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function (status) { - network._redraw(); + network._requestRedraw(); }); // keyboard navigation variables @@ -23470,6 +23472,7 @@ return /******/ (function(modules) { // webpackBootstrap util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); + this.groups.useDefaultGroups = this.constants.useDefaultGroups; if (options.physics) { util.mergeOptions(this.constants.physics, options.physics,'barnesHut'); util.mergeOptions(this.constants.physics, options.physics,'repulsion'); @@ -24765,7 +24768,23 @@ return /******/ (function(modules) { // webpackBootstrap * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. * @private */ - Network.prototype._redraw = function(hidden) { + Network.prototype._requestRedraw = function(hidden) { + if (this.redrawRequested !== true) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, hidden),0); + } + else { + window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); + } + } + }; + + Network.prototype._redraw = function(hidden, requested) { + if (hidden === undefined) { + hidden = false; + } + this.redrawRequested = false; var ctx = this.frame.canvas.getContext('2d'); ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); @@ -24789,7 +24808,7 @@ return /******/ (function(modules) { // webpackBootstrap "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) }; - if (!(hidden == true)) { + if (hidden === false) { this._doInAllSectors("_drawAllSectorNodes", ctx); if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { this._doInAllSectors("_drawEdges", ctx); @@ -24800,7 +24819,7 @@ return /******/ (function(modules) { // webpackBootstrap this._doInAllSectors("_drawNodes",ctx,false); } - if (!(hidden == true)) { + if (hidden === false) { if (this.controlNodesActive == true) { this._doInAllSectors("_drawControlNodes", ctx); } @@ -24812,10 +24831,10 @@ return /******/ (function(modules) { // webpackBootstrap // restore original scaling and translation ctx.restore(); - if (hidden == true) { + if (hidden === true) { ctx.clearRect(0, 0, w, h); } - }; + } /** * Set the translation of the network @@ -25021,10 +25040,6 @@ return /******/ (function(modules) { // webpackBootstrap var count = 0; while (this.moving && count < this.constants.stabilizationIterations) { this._physicsTick(); - // TODO: cleanup - //if (count % 100 == 0) { - // console.log("stabilizationIterations",count); - //} count++; } @@ -25206,6 +25221,11 @@ return /******/ (function(modules) { // webpackBootstrap // reset the timer so a new scheduled animation step can be set this.timer = undefined; + if (this.requiresTimeout == true) { + // this schedules a new animation step + this.start(); + } + // handle the keyboad movement this._handleNavigation(); @@ -25230,8 +25250,10 @@ return /******/ (function(modules) { // webpackBootstrap this._redraw(); this.renderTime = Date.now() - renderStartTime; - // this schedules a new animation step - this.start(); + if (this.requiresTimeout == false) { + // this schedules a new animation step + this.start(); + } }; if (typeof window !== 'undefined') { @@ -25257,7 +25279,7 @@ return /******/ (function(modules) { // webpackBootstrap } } else { - this._redraw(); + this._requestRedraw(); // this check is to ensure that the network does not emit these events if it was already stabilized and setOptions is called (setting moving to true and calling start()) if (this.stabilizationIterations > 1) { // trigger the "stabilized" event. @@ -25708,6 +25730,23 @@ return /******/ (function(modules) { // webpackBootstrap return nodeList; } + + Network.prototype.getEdgesFromNode = function(nodeId) { + var edgesList = []; + if (this.nodes[nodeId] !== undefined) { + var node = this.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgesList.push(node.edges[i].id); + } + } + return edgesList; + } + + Network.prototype.generateColorObject = function(color) { + return util.parseColor(color); + + } + module.exports = Network; @@ -25715,1394 +25754,1107 @@ return /******/ (function(modules) { // webpackBootstrap /* 52 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); - var Node = __webpack_require__(53); - /** - * @class Edge + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. * - * A edge connects two nodes - * @param {Object} properties Object with properties. Must contain - * At least properties from and to. - * Available properties: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges */ - function Edge (properties, network, networkConstants) { - if (!network) { - throw "No network provided"; - } - var fields = ['edges','physics']; - var constants = util.selectiveBridgeObject(fields,networkConstants); - this.options = constants.edges; - this.physics = constants.physics; - this.options['smoothCurves'] = networkConstants['smoothCurves']; - - - this.network = network; + function parseDOT (data) { + dot = data; + return parseGraph(); + } - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached - this.dirtyLabel = true; - this.colorDirty = true; + // token types enumeration + var TOKENTYPE = { + NULL : 0, + DELIMITER : 1, + IDENTIFIER: 2, + UNKNOWN : 3 + }; - this.from = null; // a node - this.to = null; // a node - this.via = null; // a temp node + // map with all delimiters + var DELIMITERS = { + '{': true, + '}': true, + '[': true, + ']': true, + ';': true, + '=': true, + ',': true, - this.fromBackup = null; // used to clean up after reconnect - this.toBackup = null;; // used to clean up after reconnect + '->': true, + '--': true + }; - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.originalFromId = []; - this.originalToId = []; + var dot = ''; // current dot file + var index = 0; // current index in dot file + var c = ''; // current token character in expr + var token = ''; // current token + var tokenType = TOKENTYPE.NULL; // type of the token - this.connected = false; + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } - this.widthFixed = false; - this.lengthFixed = false; + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } - this.setProperties(properties); + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } - this.controlNodesEnabled = false; - this.controlNodes = {from:null, to:null, positions:{}}; - this.connectedNode = null; + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); } /** - * Set or overwrite properties for the edge - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a */ - Edge.prototype.setProperties = function(properties) { - this.colorDirty = true; - if (!properties) { - return; + function merge (a, b) { + if (!a) { + a = {}; } - var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width', - 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity', - 'customScalingFunction','useGradients' - ]; - util.selectiveDeepExtend(fields, this.options, properties); - - if (properties.from !== undefined) {this.fromId = properties.from;} - if (properties.to !== undefined) {this.toId = properties.to;} - - if (properties.id !== undefined) {this.id = properties.id;} - if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;} - - if (properties.title !== undefined) {this.title = properties.title;} - if (properties.value !== undefined) {this.value = properties.value;} - if (properties.length !== undefined) {this.physics.springLength = properties.length;} + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + return a; + } - if (properties.color !== undefined) { - this.options.inheritColor = false; - if (util.isString(properties.color)) { - this.options.color.color = properties.color; - this.options.color.highlight = properties.color; + /** + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value + */ + function setValue(obj, path, value) { + var keys = path.split('.'); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; } else { - if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;} - if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;} - if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;} + // this is the end point + o[key] = value; } } + } + /** + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node + */ + function addNode(graph, node) { + var i, len; + var current = null; - - // A node is connected when it has a from and to node. - this.connect(); - - this.widthFixed = this.widthFixed || (properties.width !== undefined); - this.lengthFixed = this.lengthFixed || (properties.length !== undefined); - - this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; - - // set draw method based on style - switch (this.options.style) { - case 'line': this.draw = this._drawLine; break; - case 'arrow': this.draw = this._drawArrow; break; - case 'arrow-center': this.draw = this._drawArrowCenter; break; - case 'dash-line': this.draw = this._drawDashLine; break; - default: this.draw = this._drawLine; break; + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; } - }; + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; + } + } + } - /** - * Connect an edge to its nodes - */ - Edge.prototype.connect = function () { - this.disconnect(); + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } + } - this.from = this.network.nodes[this.fromId] || null; - this.to = this.network.nodes[this.toId] || null; - this.connected = (this.from && this.to); + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - if (this.connected) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } - else { - if (this.from) { - this.from.detachEdge(this); + if (!g.nodes) { + g.nodes = []; } - if (this.to) { - this.to.detachEdge(this); + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); } } - }; - /** - * Disconnect an edge from its nodes - */ - Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); } - - this.connected = false; - }; + } /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge */ - Edge.prototype.getTitle = function() { - return typeof this.title === "function" ? this.title() : this.title; - }; - + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; + } + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes + } + } /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge */ - Edge.prototype.getValue = function() { - return this.value; - }; + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - */ - Edge.prototype.setValueRange = function(min, max, total) { - if (!this.widthFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.widthMax - this.options.widthMin; - this.options.width = this.options.widthMin + scale * widthDiff; - this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes } - }; + edge.attr = merge(edge.attr || {}, attr); // merge attributes - /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - Edge.prototype.draw = function(ctx) { - throw "Method draw not initialized in edge"; - }; + return edge; + } /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge + * Get next token in the current dot file. + * The token and token type are available as token and tokenType */ - Edge.prototype.isOverlappingWith = function(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - - var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ''; - return (dist < distMax); - } - else { - return false + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); } - }; - - Edge.prototype._getColor = function(ctx) { - var colorObj = this.options.color; - if (this.options.useGradients == true) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor, toColor; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; + do { + var isComment = false; - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); + // skip comment + if (c == '#') { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { + i--; + } + if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { + // the # is at the start of a line, this is indeed a line comment + while (c != '' && c != '\n') { + next(); + } + isComment = true; + } } - else if (this.from.selected == true && this.to.selected == false) { - toColor = this.to.options.color.border; + if (c == '/' && nextPreview() == '/') { + // skip line comment + while (c != '' && c != '\n') { + next(); + } + isComment = true; } - else if (this.from.selected == false && this.to.selected == true) { - fromColor = this.from.options.color.border; + if (c == '/' && nextPreview() == '*') { + // skip block comment + while (c != '') { + if (c == '*' && nextPreview() == '/') { + // end of block comment found. skip these last two characters + next(); + next(); + break; + } + else { + next(); + } + } + isComment = true; } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); - return grd; - } - if (this.colorDirty === true) { - if (this.options.inheritColor == "to") { - colorObj = { - highlight: this.to.options.color.highlight.border, - hover: this.to.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } - else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { - colorObj = { - highlight: this.from.options.color.highlight.border, - hover: this.from.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; + // skip over whitespaces + while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter + next(); } - this.options.color = colorObj; - this.colorDirty = false; } + while (isComment); + // check for end of dot file + if (c == '') { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } - if (this.selected == true) {return colorObj.highlight;} - else if (this.hover == true) {return colorObj.hover;} - else {return colorObj.color;} - }; - + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawLine = function(ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == '-') { + token += c; + next(); - // draw label - var point; - if (this.label) { - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); + while (isAlphaNumeric(c)) { + token += c; + next(); } - } - else { - var x, y; - var radius = this.physics.springLength / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); + if (token == 'false') { + token = false; // convert to boolean } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; + else if (token == 'true') { + token = true; // convert to boolean } - else { - x = node.x + radius; - y = node.y - node.height / 2; + else if (!isNaN(Number(token))) { + token = Number(token); // convert to number } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + tokenType = TOKENTYPE.IDENTIFIER; + return; } - }; - /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width - * @private - */ - Edge.prototype._getLineWidth = function() { - if (this.selected == true) { - return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv); - } - else { - if (this.hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv); + // check for a string enclosed by double quotes + if (c == '"') { + next(); + while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { + token += c; + if (c == '"') { // skip the escape character + next(); + } + next(); } - else { - return Math.max(this.options.width, 0.3*this.networkScaleInv); + if (c != '"') { + throw newSyntaxError('End of string " expected'); } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; } - }; - Edge.prototype._getViaCoordinates = function () { - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) { - return this.via; - } - else if (this.options.smoothCurves.enabled == false) { - return {x:0,y:0}; + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != '') { + token += c; + next(); } - else { - var xVia = null; - var yVia = null; - var factor = this.options.smoothCurves.roundness; - var type = this.options.smoothCurves.type; + throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); + } - var dx = Math.abs(this.from.x - this.to.x); - var dy = Math.abs(this.from.y - this.to.y); - if (type == 'discrete' || type == 'diagonalCross') { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - } - } - if (type == "discrete") { - xVia = dx < factor * dy ? this.from.x : xVia; - } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - } - } - if (type == "discrete") { - yVia = dy < factor * dx ? this.from.y : yVia; - } - } - } - else if (type == "straightCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } - else { - yVia = this.to.y + (1 - factor) * dy; - } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } - else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } - } - else if (type == 'horizontal') { - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } - else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } - else if (type == 'vertical') { - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } - else { - yVia = this.to.y + (1 - factor) * dy; - } - } - else { // continuous - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - // console.log(1) - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } - else if (this.from.x > this.to.x) { - // console.log(2) - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - // console.log(3) - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } - else if (this.from.x > this.to.x) { - // console.log(4, this.from.x, this.to.x) - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; - } - } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - // console.log(5) - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { - // console.log(6) - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - // console.log(7) - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { - // console.log(8) - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } - } - } - } + /** + * Parse a graph. + * @returns {Object} graph + */ + function parseGraph() { + var graph = {}; + first(); + getToken(); - return {x: xVia, y: yVia}; + // optional strict keyword + if (token == 'strict') { + graph.strict = true; + getToken(); } - }; - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - if (this.options.smoothCurves.enabled == true) { - if (this.options.smoothCurves.dynamic == false) { - var via = this._getViaCoordinates(); - if (via.x == null) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; - } - else { - // this.via.x = via.x; - // this.via.y = via.y; - ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); - ctx.stroke(); - return via; - } - } - else { - ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); - ctx.stroke(); - return this.via; - } - } - else { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; + // graph or digraph keyword + if (token == 'graph' || token == 'digraph') { + graph.type = token; + getToken(); } - }; - - /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @private - */ - Edge.prototype._circle = function (ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - }; - /** - * Draw label with white background and with the middle at (x, y) - * @param {CanvasRenderingContext2D} ctx - * @param {String} text - * @param {Number} x - * @param {Number} y - * @private - */ - Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + - this.options.fontSize + "px " + this.options.fontFace; - var yLine; - - if (this.dirtyLabel == true) { - var lines = String(text).split('\n'); - var lineCount = lines.length; - var fontSize = Number(this.options.fontSize); - yLine = y + (1 - lineCount) / 2 * fontSize; + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - var width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } - var height = this.options.fontSize * lineCount; - var left = x - width / 2; - var top = y - height / 2; + // open angle bracket + if (token != '{') { + throw newSyntaxError('Angle bracket { expected'); + } + getToken(); - // cache - this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine}; - } + // statements + parseStatements(graph); - var yLine = this.labelDimensions.yLine; - - ctx.save(); - - if (this.options.labelAlignment != "horizontal"){ - ctx.translate(x, yLine); - this._rotateForLabelAlignment(ctx); - x = 0; - yLine = 0; - } + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); + } + getToken(); - - this._drawLabelRect(ctx); - this._drawLabelText(ctx,x,yLine, lines, lineCount, fontSize); - - ctx.restore(); + // end of file + if (token !== '') { + throw newSyntaxError('End of file expected'); } - }; + getToken(); - /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._rotateForLabelAlignment = function(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; - // rotate so label it is readable - if((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)){ - angleInDegrees = angleInDegrees + Math.PI; - } - - ctx.rotate(angleInDegrees); - }; + return graph; + } /** - * Draws the label rectangle - * @param {CanvasRenderingContext2D} ctx - * @param {String} labelAlignment - * @private + * Parse a list with statements. + * @param {Object} graph */ - Edge.prototype._drawLabelRect = function(ctx) { - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; - - var lineMargin = 2; - - if (this.options.labelAlignment == 'line-center') { - ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); - } - else if (this.options.labelAlignment == 'line-above') { - ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); - } - else if (this.options.labelAlignment == 'line-below') { - ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); - } - else { - ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); + function parseStatements (graph) { + while (token !== '' && token != '}') { + parseStatement(graph); + if (token == ';') { + getToken(); } } - }; + } /** - * Draws the label text - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} yLine - * @param {Array} lines - * @param {Number} lineCount - * @param {Number} fontSize - * @private + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph */ - Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSize) { - // draw text - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = "center"; + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); - // check for label alignment - if (this.options.labelAlignment != 'horizontal') { - var lineMargin = 2; - if (this.options.labelAlignment == 'line-above') { - ctx.textBaseline = "alphabetic"; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } - else if (this.options.labelAlignment == 'line-below') { - ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers - } - else { - ctx.textBaseline = "middle"; - } - } - else { - ctx.textBaseline = "middle"; + return; } - // check for strokeWidth - if (this.options.fontStrokeWidth > 0){ - ctx.lineWidth = this.options.fontStrokeWidth; - ctx.strokeStyle = this.options.fontStrokeColor; - ctx.lineJoin = 'round'; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; } - for (var i = 0; i < lineCount; i++) { - if(this.options.fontStrokeWidth > 0){ - ctx.strokeText(lines[i], x, yLine); - } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } - }; - /** - * Redraw a edge as a dashed line - * Draw this edge in the given canvas - * @author David Jordan - * @date 2012-08-08 - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawDashLine = function(ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); - - var via = null; - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { - ctx.save(); - // configure the dash pattern - var pattern = [0]; - if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { - pattern = [this.options.dash.length,this.options.dash.gap]; - } - else { - pattern = [5,5]; - } - - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; - - // draw the line - via = this._line(ctx); - - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } - else { // unsupporting smooth lines - // draw dashed line - ctx.beginPath(); - ctx.lineCap = 'round'; - if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]); - } - else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap]); - } - else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); } + var id = token; // id can be a string or a number + getToken(); - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { - point = this._pointOnLine(0.5); + if (token == '=') { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier expected'); } - this._label(ctx, this.label, point.x, point.y); + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " } - }; - - /** - * Get a point on a line - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - Edge.prototype._pointOnLine = function (percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y + else { + parseNodeStatement(graph, id); } - }; + } /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph */ - Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3/8) * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - } - }; + function parseSubgraph (graph) { + var subgraph = null; - /** - * Redraw a edge as a line with an arrow halfway the line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._drawArrowCenter = function(ctx) { - var point; - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); + // optional subgraph keyword + if (token == 'subgraph') { + subgraph = {}; + subgraph.type = 'subgraph'; + getToken(); - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } + } - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - // draw an arrow halfway the line - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; + // open angle bracket + if (token == '{') { + getToken(); + + if (!subgraph) { + subgraph = {}; } - else { - point = this._pointOnLine(0.5); + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; + + // statements + parseStatements(subgraph); + + // close angle bracket + if (token != '}') { + throw newSyntaxError('Angle bracket } expected'); } + getToken(); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; - // draw label - if (this.label) { - this._label(ctx, this.label, point.x, point.y); + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; } + graph.subgraphs.push(subgraph); } - else { - // draw circle - var x, y; - var radius = 0.25 * Math.max(100,this.physics.springLength); - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height * 0.5; - } - this._circle(ctx, x, y, radius); - // draw all arrows - var angle = 0.2 * Math.PI; - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + return subgraph; + } + + /** + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. + */ + function parseAttributeStatement (graph) { + // attribute statements + if (token == 'node') { + getToken(); - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + // node attributes + graph.node = parseAttributeList(); + return 'node'; } - }; + else if (token == 'edge') { + getToken(); - Edge.prototype._pointOnBezier = function(t) { - var via = this._getViaCoordinates(); + // edge attributes + graph.edge = parseAttributeList(); + return 'edge'; + } + else if (token == 'graph') { + getToken(); - var x = Math.pow(1-t,2)*this.from.x + (2*t*(1 - t))*via.x + Math.pow(t,2)*this.to.x; - var y = Math.pow(1-t,2)*this.from.y + (2*t*(1 - t))*via.y + Math.pow(t,2)*this.to.y; + // graph attributes + graph.graph = parseAttributeList(); + return 'graph'; + } - return {x:x,y:y}; + return null; } /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param from - * @param ctx - * @returns {*} - * @private + * parse a node statement + * @param {Object} graph + * @param {String | Number} id */ - Edge.prototype._findBorderPosition = function(from,ctx) { - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos,angle,distanceToBorder, distanceToNodes, difference; - var threshold = 0.2; - var node = this.to; - if (from == true) { - node = this.from; + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; } + addNode(graph, node); - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; + // edge statements + parseEdge(graph, id); + } - pos = this._pointOnBezier(middle); - angle = Math.atan2((node.y - pos.y), (node.x - pos.x)); - distanceToBorder = node.distanceToBorder(ctx,angle); - distanceToNodes = Math.sqrt(Math.pow(pos.x-node.x,2) + Math.pow(pos.y-node.y,2)); - difference = distanceToBorder - distanceToNodes; - if (Math.abs(difference) < threshold) { - break; // found - } - else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (from == false) { - low = middle; - } - else { - high = middle; - } + /** + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node + */ + function parseEdge(graph, from) { + while (token == '->' || token == '--') { + var to; + var type = token; + getToken(); + + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; } else { - if (from == false) { - high = middle; - } - else { - low = middle; + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Identifier or subgraph expected'); } + to = token; + addNode(graph, { + id: to + }); + getToken(); } - iteration++; - } - pos.t = middle; + // parse edge attributes + var attr = parseAttributeList(); - return pos; - }; + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); + + from = to; + } + } /** - * Redraw a edge as a line with an arrow - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr */ - Edge.prototype._drawArrow = function(ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); + function parseAttributeList() { + var attr = null; - // set vars - var angle, length, arrowPos; + while (token == '[') { + getToken(); + attr = {}; + while (token !== '' && token != ']') { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute name expected'); + } + var name = token; - // if not connected to itself - if (this.from != this.to) { - // draw line - this._line(ctx); + getToken(); + if (token != '=') { + throw newSyntaxError('Equal sign = expected'); + } + getToken(); - // draw arrow head - if (this.options.smoothCurves.enabled == true) { - var via = this._getViaCoordinates(); - arrowPos = this._findBorderPosition(false, ctx); - var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1)) - angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x)); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError('Attribute value expected'); + } + var value = token; + setValue(attr, name, value); // name can be a path + + getToken(); + if (token ==',') { + getToken(); + } } - else { - angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - arrowPos = {}; - arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + if (token != ']') { + throw newSyntaxError('Bracket ] expected'); } + getToken(); + } - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrowPos.x,arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); + return attr; + } - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - point = this._pointOnBezier(0.5); + /** + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err + */ + function newSyntaxError(message) { + return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); + } + + /** + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} + */ + function chop (text, maxLength) { + return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); + } + + /** + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn + */ + function forEach2(array1, array2, fn) { + if (Array.isArray(array1)) { + array1.forEach(function (elem1) { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); } else { - point = this._pointOnLine(0.5); + fn(elem1, array2); } - this._label(ctx, this.label, point.x, point.y); - } + }); } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = 0.25 * Math.max(100,this.physics.springLength); - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - arrow = { - x: x, - y: node.y, - angle: 0.9 * Math.PI - }; + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); } else { - x = node.x + radius; - y = node.y - node.height * 0.5; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; - } - ctx.beginPath(); - // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - - // draw all arrows - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); - - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + fn(array1, array2); } } - }; + } /** - * Calculate the distance between a point (x3,y3) and a line segment from - * (x1,y1) to (x2,y2). - * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment - * @param {number} x1 - * @param {number} y1 - * @param {number} x2 - * @param {number} y2 - * @param {number} x3 - * @param {number} y3 - * @private + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData */ - Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - if (this.options.smoothCurves.enabled == true) { - var xVia, yVia; - if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { - xVia = this.via.x; - yVia = this.via.y; + function DOTToGraph (data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; + + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = 'image'; + } + graphData.nodes.push(graphNode); + }); + } + + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + var convertEdge = function (dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; + return graphEdge; + } + + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; } else { - var via = this._getViaCoordinates(); - xVia = via.x; - yVia = via.y; + from = { + id: dotEdge.from + } } - var minDistance = 1e9; - var distance; - var i,t,x,y, lastX, lastY; - for (i = 0; i < 10; i++) { - t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3); - minDistance = distance < minDistance ? distance : minDistance; + + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } + else { + to = { + id: dotEdge.to } - lastX = x; lastY = y; } - returnValue = minDistance; - } - else { - returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3); + + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } + + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } + + return graphData; + } + + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; + + +/***/ }, +/* 53 */ +/***/ function(module, exports, __webpack_require__) { + + + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true + }, + nodes: { + allowedToMove: false, + parseColor: false } + }; + + if (options !== undefined) { + this.options.nodes['allowedToMove'] = options.allowedToMove | false; + this.options.nodes['parseColor'] = options.parseColor | false; + this.options.edges['inheritColor'] = options.inheritColor | true; } - else { - var x, y, dx, dy; - var radius = 0.25 * this.physics.springLength; - var node = this.from; - if (node.width > node.height) { - x = node.x + 0.5 * node.width; - y = node.y - radius; + + var gEdges = gephiJSON.edges; + var gNodes = gephiJSON.nodes; + for (var i = 0; i < gEdges.length; i++) { + var edge = {}; + var gEdge = gEdges[i]; + edge['id'] = gEdge.id; + edge['from'] = gEdge.source; + edge['to'] = gEdge.target; + edge['attributes'] = gEdge.attributes; + // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; + // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; + edge['color'] = gEdge.color; + edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor; + edges.push(edge); + } + + for (var i = 0; i < gNodes.length; i++) { + var node = {}; + var gNode = gNodes[i]; + node['id'] = gNode.id; + node['attributes'] = gNode.attributes; + node['x'] = gNode.x; + node['y'] = gNode.y; + node['label'] = gNode.label; + if (this.options.nodes.parseColor == true) { + node['color'] = gNode.color; } else { - x = node.x + radius; - y = node.y - 0.5 * node.height; + node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined; } - dx = x - x3; - dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius); + node['radius'] = gNode.size; + node['allowedToMoveX'] = this.options.nodes.allowedToMove; + node['allowedToMoveY'] = this.options.nodes.allowedToMove; + nodes.push(node); } - if (this.labelDimensions.left < x3 && - this.labelDimensions.left + this.labelDimensions.width > x3 && - this.labelDimensions.top < y3 && - this.labelDimensions.top + this.labelDimensions.height > y3) { - return 0; - } - else { - return returnValue; - } - }; + return {nodes:nodes, edges:edges}; + } - Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) { - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + exports.parseGephi = parseGephi; - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; - } +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + var util = __webpack_require__(1); - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + /** + * @class Groups + * This class can store groups and properties specific for groups. + */ + function Groups() { + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + this.useDefaultGroups = true; + } - return Math.sqrt(dx*dx + dy*dy); - }; /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * default constants for group colors */ - Edge.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; - }; - - - Edge.prototype.select = function() { - this.selected = true; - }; + Groups.DEFAULT = [ + {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue + {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow + {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red + {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green + {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta + {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple + {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange + {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue + {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink + {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint + + {border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red + {border: "#01AA01", background: "#22FF22", highlight: {border: "#33DD33", background: "#AAFFAA"}, hover: {border: "#22FF22", background: "#66FF66"}}, // 11:bright GREEN + + {border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 12: blue + {border: "#FFFF00", background: "#FFA500", highlight: {border: "#FFFFA3", background: "#FFA500"}, hover: {border: "#FFFFA3", background: "#FFA500"}}, // 13: yellow + //{border: "#FB7E81", background: "#FA0A10", highlight: {border: "#FFAFB1", background: "#FA0A10"}, hover: {border: "#FFAFB1", background: "#FA0A10"}}, // 14: red + {border: "#7BE141", background: "#41A906", highlight: {border: "#A1EC76", background: "#41A906"}, hover: {border: "#A1EC76", background: "#41A906"}}, // 15: green + {border: "#EB7DF4", background: "#E129F0", highlight: {border: "#F0B3F5", background: "#E129F0"}, hover: {border: "#F0B3F5", background: "#E129F0"}}, // 16: magenta + {border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 17: purple + {border: "#FFA807", background: "#C37F00", highlight: {border: "#FFCA66", background: "#C37F00"}, hover: {border: "#FFCA66", background: "#C37F00"}}, // 18: orange + {border: "#6E6EFD", background: "#4220FB", highlight: {border: "#9B9BFD", background: "#4220FB"}, hover: {border: "#9B9BFD", background: "#4220FB"}}, // 19: darkblue + {border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 20: pink + {border: "#C2FABC", background: "#4AD63A", highlight: {border: "#E6FFE3", background: "#4AD63A"}, hover: {border: "#E6FFE3", background: "#4AD63A"}}, // 21:mint + + {border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 22:bright red + {border: "#22FF22", background: "#01AA01", highlight: {border: "#AAFFAA", background: "#33DD33"}, hover: {border: "#66FF66", background: "#22FF22"}}, // 23:bright GREEN + ]; - Edge.prototype.unselect = function() { - this.selected = false; - }; - Edge.prototype.positionBezierNode = function() { - if (this.via !== null && this.from !== null && this.to !== null) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } - else if (this.via !== null) { - this.via.x = 0; - this.via.y = 0; + /** + * Clear all groups + */ + Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } + } + return i; } }; + /** - * This function draws the control nodes for the manipulator. - * In order to enable this, only set the this.controlNodesEnabled to true. - * @param ctx + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties */ - Edge.prototype._drawControlNodes = function(ctx) { - if (this.controlNodesEnabled == true) { - if (this.controlNodes.from === null && this.controlNodes.to === null) { - var nodeIdFrom = "edgeIdFrom:".concat(this.id); - var nodeIdTo = "edgeIdTo:".concat(this.id); - var constants = { - nodes:{group:'', radius:7, borderWidth:2, borderWidthSelected: 2}, - physics:{damping:0}, - clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}} - }; - this.controlNodes.from = new Node( - {id:nodeIdFrom, - shape:'dot', - color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); - this.controlNodes.to = new Node( - {id:nodeIdTo, - shape:'dot', - color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); - } - - this.controlNodes.positions = {}; - if (this.controlNodes.from.selected == false) { - this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); - this.controlNodes.from.x = this.controlNodes.positions.from.x; - this.controlNodes.from.y = this.controlNodes.positions.from.y; + Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; } - if (this.controlNodes.to.selected == false) { - this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); - this.controlNodes.to.x = this.controlNodes.positions.to.x; - this.controlNodes.to.y = this.controlNodes.positions.to.y; + else { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; } - - this.controlNodes.from.draw(ctx); - this.controlNodes.to.draw(ctx); - } - else { - this.controlNodes = {from:null, to:null, positions:{}}; } + + return group; }; /** - * Enable control nodes. - * @private + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object */ - Edge.prototype._enableControlNodes = function() { - this.fromBackup = this.from; - this.toBackup = this.to; - this.controlNodesEnabled = true; + Groups.prototype.add = function (groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; }; - /** - * disable control nodes and remove from dynamicEdges from old node - * @private - */ - Edge.prototype._disableControlNodes = function() { - this.fromId = this.from.id; - this.toId = this.to.id; - if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges - this.fromBackup.detachEdge(this); - } - else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges - this.toBackup.detachEdge(this); - } + module.exports = Groups; - this.fromBackup = null; - this.toBackup = null; - this.controlNodesEnabled = false; - }; +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { /** - * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. - * @param x - * @param y - * @returns {null} - * @private + * @class Images + * This class loads images and keeps them stored. */ - Edge.prototype._getSelectedControlNode = function(x,y) { - var positions = this.controlNodes.positions; - var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2)); - var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2)); - - if (fromDistance < 15) { - this.connectedNode = this.from; - this.from = this.controlNodes.from; - return this.controlNodes.from; - } - else if (toDistance < 15) { - this.connectedNode = this.to; - this.to = this.controlNodes.to; - return this.controlNodes.to; - } - else { - return null; - } - }; - + function Images() { + this.images = {}; + this.imageBroken = {}; + this.callback = undefined; + } /** - * this resets the control nodes to their original position. - * @private + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback */ - Edge.prototype._restoreControlNodes = function() { - if (this.controlNodes.from.selected == true) { - this.from = this.connectedNode; - this.connectedNode = null; - this.controlNodes.from.unselect(); - } - else if (this.controlNodes.to.selected == true) { - this.to = this.connectedNode; - this.connectedNode = null; - this.controlNodes.to.unselect(); - } + Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; }; /** - * this calculates the position of the control nodes on the edges of the parent nodes. * - * @param ctx - * @returns {x: *, y: *} + * @param {string} url Url of the image + * @param {string} url Url of an image to use if the url image is not found + * @return {Image} img The image object */ - Edge.prototype.getControlNodeFromPosition = function(ctx) { - // draw arrow head - var controlnodeFromPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeFromPos = this._findBorderPosition(true, ctx); - } - else { - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - - var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); - var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; - controlnodeFromPos = {}; - controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; - controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; - } + Images.prototype.load = function(url, brokenUrl) { + var img = this.images[url]; // make a pointer + if (img === undefined) { + // create the image + var me = this; + img = new Image(); + img.onload = function () { + // IE11 fix -- thanks dponch! + if (this.width == 0) { + document.body.appendChild(this); + this.width = this.offsetWidth; + this.height = this.offsetHeight; + document.body.removeChild(this); + } - return controlnodeFromPos; - }; + if (me.callback) { + me.images[url] = img; + me.callback(this); + } + }; - /** - * this calculates the position of the control nodes on the edges of the parent nodes. - * - * @param ctx - * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} - */ - Edge.prototype.getControlNodeToPosition = function(ctx) { - // draw arrow head - var controlnodeFromPos,controlnodeToPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeToPos = this._findBorderPosition(false, ctx); - } - else { - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); + } + } + else { + if (me.imageBroken[url] === true) { + if (this.src == brokenUrl) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); + } + } + else { + console.error("Could not load image:", url); + this.src = brokenUrl; + } + } + else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; + } + } + }; - controlnodeToPos = {}; - controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + img.src = url; } - return controlnodeToPos; + return img; }; - module.exports = Edge; + module.exports = Images; + /***/ }, -/* 53 */ +/* 56 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -27185,7 +26937,7 @@ return /******/ (function(modules) { // webpackBootstrap this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width; this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height; this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius; - this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements; + this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements; this.growthIndicator = 0; // variables to tell the node about the network. @@ -28267,1320 +28019,1655 @@ return /******/ (function(modules) { // webpackBootstrap width = Math.max(width, ctx.measureText(lines[i]).width); } - return {"width": width, "height": height, lineCount: lines.length}; + return {"width": width, "height": height, lineCount: lines.length}; + } + else { + return {"width": 0, "height": 0, lineCount: 0}; + } + }; + + /** + * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. + * there is a safety margin of 0.3 * width; + * + * @returns {boolean} + */ + Node.prototype.inArea = function() { + if (this.width !== undefined) { + return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x && + this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x && + this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y && + this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y); + } + else { + return true; + } + }; + + /** + * checks if the core of the node is in the display area, this is used for opening clusters around zoom + * @returns {boolean} + */ + Node.prototype.inView = function() { + return (this.x >= this.canvasTopLeft.x && + this.x < this.canvasBottomRight.x && + this.y >= this.canvasTopLeft.y && + this.y < this.canvasBottomRight.y); + }; + + /** + * This allows the zoom level of the network to influence the rendering + * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas + * + * @param scale + * @param canvasTopLeft + * @param canvasBottomRight + */ + Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { + this.networkScaleInv = 1.0/scale; + this.networkScale = scale; + this.canvasTopLeft = canvasTopLeft; + this.canvasBottomRight = canvasBottomRight; + }; + + + /** + * This allows the zoom level of the network to influence the rendering + * + * @param scale + */ + Node.prototype.setScale = function(scale) { + this.networkScaleInv = 1.0/scale; + this.networkScale = scale; + }; + + + + /** + * set the velocity at 0. Is called when this node is contained in another during clustering + */ + Node.prototype.clearVelocity = function() { + this.vx = 0; + this.vy = 0; + }; + + + /** + * Basic preservation of (kinectic) energy + * + * @param massBeforeClustering + */ + Node.prototype.updateVelocity = function(massBeforeClustering) { + var energyBefore = this.vx * this.vx * massBeforeClustering; + //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vx = Math.sqrt(energyBefore/this.options.mass); + energyBefore = this.vy * this.vy * massBeforeClustering; + //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vy = Math.sqrt(energyBefore/this.options.mass); + }; + + module.exports = Node; + + +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + var Node = __webpack_require__(56); + + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ + function Edge (properties, network, networkConstants) { + if (!network) { + throw "No network provided"; + } + var fields = ['edges','physics']; + var constants = util.selectiveBridgeObject(fields,networkConstants); + this.options = constants.edges; + this.physics = constants.physics; + this.options['smoothCurves'] = networkConstants['smoothCurves']; + + + this.network = network; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.title = undefined; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached + this.dirtyLabel = true; + this.colorDirty = true; + + this.from = null; // a node + this.to = null; // a node + this.via = null; // a temp node + + this.fromBackup = null; // used to clean up after reconnect + this.toBackup = null;; // used to clean up after reconnect + + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.originalFromId = []; + this.originalToId = []; + + this.connected = false; + + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties); + + this.controlNodesEnabled = false; + this.controlNodes = {from:null, to:null, positions:{}}; + this.connectedNode = null; + } + + /** + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ + Edge.prototype.setProperties = function(properties) { + this.colorDirty = true; + if (!properties) { + return; + } + + var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width', + 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity', + 'customScalingFunction','useGradients' + ]; + util.selectiveDeepExtend(fields, this.options, properties); + + if (properties.from !== undefined) {this.fromId = properties.from;} + if (properties.to !== undefined) {this.toId = properties.to;} + + if (properties.id !== undefined) {this.id = properties.id;} + if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;} + + if (properties.title !== undefined) {this.title = properties.title;} + if (properties.value !== undefined) {this.value = properties.value;} + if (properties.length !== undefined) {this.physics.springLength = properties.length;} + + if (properties.color !== undefined) { + this.options.inheritColor = false; + if (util.isString(properties.color)) { + this.options.color.color = properties.color; + this.options.color.highlight = properties.color; + } + else { + if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;} + if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;} + if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;} + } + } + + + + // A node is connected when it has a from and to node. + this.connect(); + + this.widthFixed = this.widthFixed || (properties.width !== undefined); + this.lengthFixed = this.lengthFixed || (properties.length !== undefined); + + this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; + + // set draw method based on style + switch (this.options.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-center': this.draw = this._drawArrowCenter; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } + }; + + + /** + * Connect an edge to its nodes + */ + Edge.prototype.connect = function () { + this.disconnect(); + + this.from = this.network.nodes[this.fromId] || null; + this.to = this.network.nodes[this.toId] || null; + this.connected = (this.from && this.to); + + if (this.connected) { + this.from.attachEdge(this); + this.to.attachEdge(this); } else { - return {"width": 0, "height": 0, lineCount: 0}; + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); + } } }; /** - * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. - * there is a safety margin of 0.3 * width; - * - * @returns {boolean} + * Disconnect an edge from its nodes */ - Node.prototype.inArea = function() { - if (this.width !== undefined) { - return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x && - this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x && - this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y && - this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y); + Edge.prototype.disconnect = function () { + if (this.from) { + this.from.detachEdge(this); + this.from = null; } - else { - return true; + if (this.to) { + this.to.detachEdge(this); + this.to = null; } + + this.connected = false; }; /** - * checks if the core of the node is in the display area, this is used for opening clusters around zoom - * @returns {boolean} + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. */ - Node.prototype.inView = function() { - return (this.x >= this.canvasTopLeft.x && - this.x < this.canvasBottomRight.x && - this.y >= this.canvasTopLeft.y && - this.y < this.canvasBottomRight.y); + Edge.prototype.getTitle = function() { + return typeof this.title === "function" ? this.title() : this.title; }; + /** - * This allows the zoom level of the network to influence the rendering - * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas - * - * @param scale - * @param canvasTopLeft - * @param canvasBottomRight + * Retrieve the value of the edge. Can be undefined + * @return {Number} value */ - Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; - this.canvasTopLeft = canvasTopLeft; - this.canvasBottomRight = canvasBottomRight; + Edge.prototype.getValue = function() { + return this.value; }; - /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max */ - Node.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; + Edge.prototype.setValueRange = function(min, max, total) { + if (!this.widthFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.widthMax - this.options.widthMin; + this.options.width = this.options.widthMin + scale * widthDiff; + this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; + } }; - - /** - * set the velocity at 0. Is called when this node is contained in another during clustering + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - Node.prototype.clearVelocity = function() { - this.vx = 0; - this.vy = 0; + Edge.prototype.draw = function(ctx) { + throw "Method draw not initialized in edge"; }; - /** - * Basic preservation of (kinectic) energy - * - * @param massBeforeClustering + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge */ - Node.prototype.updateVelocity = function(massBeforeClustering) { - var energyBefore = this.vx * this.vx * massBeforeClustering; - //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vx = Math.sqrt(energyBefore/this.options.mass); - energyBefore = this.vy * this.vy * massBeforeClustering; - //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vy = Math.sqrt(energyBefore/this.options.mass); + Edge.prototype.isOverlappingWith = function(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; + + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); + } + else { + return false + } }; - module.exports = Node; + Edge.prototype._getColor = function(ctx) { + var colorObj = this.options.color; + if (this.options.useGradients == true) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor, toColor; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; -/***/ }, -/* 54 */ -/***/ function(module, exports, __webpack_require__) { + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity); + } + else if (this.from.selected == true && this.to.selected == false) { + toColor = this.to.options.color.border; + } + else if (this.from.selected == false && this.to.selected == true) { + fromColor = this.from.options.color.border; + } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); + return grd; + } - var util = __webpack_require__(1); + if (this.colorDirty === true) { + if (this.options.inheritColor == "to") { + colorObj = { + highlight: this.to.options.color.highlight.border, + hover: this.to.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } + else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { + colorObj = { + highlight: this.from.options.color.highlight.border, + hover: this.from.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } + this.options.color = colorObj; + this.colorDirty = false; + } - /** - * @class Groups - * This class can store groups and properties specific for groups. - */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - } - /** - * default constants for group colors - */ - Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint - ]; + if (this.selected == true) {return colorObj.highlight;} + else if (this.hover == true) {return colorObj.hover;} + else {return colorObj.color;} + }; /** - * Clear all groups + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; + Edge.prototype._drawLine = function(ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + var via = this._line(ctx); + + // draw label + var point; + if (this.label) { + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); + point = {x:midpointX, y:midpointY}; + } + else { + point = this._pointOnLine(0.5); } + this._label(ctx, this.label, point.x, point.y); } - return i; } - }; - - - /** - * get group properties of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group properties - */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; + else { + var x, y; + var radius = this.physics.springLength / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } - - return group; }; /** - * Add a custom group style - * @param {String} groupname - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private */ - Groups.prototype.add = function (groupname, style) { - this.groups[groupname] = style; - return style; + Edge.prototype._getLineWidth = function() { + if (this.selected == true) { + return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv); + } + else { + if (this.hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv); + } + else { + return Math.max(this.options.width, 0.3*this.networkScaleInv); + } + } }; - module.exports = Groups; - - -/***/ }, -/* 55 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * @class Images - * This class loads images and keeps them stored. - */ - function Images() { - this.images = {}; - this.imageBroken = {}; - this.callback = undefined; - } + Edge.prototype._getViaCoordinates = function () { + if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) { + return this.via; + } + else if (this.options.smoothCurves.enabled == false) { + return {x:0,y:0}; + } + else { + var xVia = null; + var yVia = null; + var factor = this.options.smoothCurves.roundness; + var type = this.options.smoothCurves.type; + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type == 'discrete' || type == 'diagonalCross') { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type == "discrete") { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } + else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } + else { + yVia = this.to.y + (1 - factor) * dy; + } + } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } + else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } + } + else if (type == 'horizontal') { + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } + else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } + else if (type == 'vertical') { + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } + else { + yVia = this.to.y + (1 - factor) * dy; + } + } + else if (type == 'curvedCW') { + var dx = this.to.x - this.from.x; + var dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx*dx + dy*dy); + var pi = Math.PI; - /** - * Set an onload callback function. This will be called each time an image - * is loaded - * @param {function} callback - */ - Images.prototype.setOnloadCallback = function(callback) { - this.callback = callback; - }; + var originalAngle = Math.atan2(dy,dx); + var myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi); - /** - * - * @param {string} url Url of the image - * @param {string} url Url of an image to use if the url image is not found - * @return {Image} img The image object - */ - Images.prototype.load = function(url, brokenUrl) { - var img = this.images[url]; // make a pointer - if (img === undefined) { - // create the image - var me = this; - img = new Image(); - img.onload = function () { - // IE11 fix -- thanks dponch! - if (this.width == 0) { - document.body.appendChild(this); - this.width = this.offsetWidth; - this.height = this.offsetHeight; - document.body.removeChild(this); - } + xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle); + yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle); + } + else if (type == 'curvedCCW') { + var dx = this.to.x - this.from.x; + var dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx*dx + dy*dy); + var pi = Math.PI; - if (me.callback) { - me.images[url] = img; - me.callback(this); - } - }; + var originalAngle = Math.atan2(dy,dx); + var myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi); - img.onerror = function () { - if (brokenUrl === undefined) { - console.error("Could not load image:", url); - delete this.src; - if (me.callback) { - me.callback(this); + xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle); + yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle); + } + else { // continuous + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } + } + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x < xVia ? this.to.x : xVia; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } } } - else { - if (me.imageBroken[url] === true) { - if (this.src == brokenUrl) { - console.error("Could not load brokenImage:", brokenUrl); - delete this.src; - if (me.callback) { - me.callback(this); - } + else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; } - else { - console.error("Could not load image:", url); - this.src = brokenUrl; + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + yVia = this.to.y > yVia ? this.to.y : yVia; } } - else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; + else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } + else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } } } - }; - - img.src = url; + } + + + return {x: xVia, y: yVia}; + } + }; + + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + if (this.options.smoothCurves.enabled == true) { + if (this.options.smoothCurves.dynamic == false) { + var via = this._getViaCoordinates(); + if (via.x == null) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } + else { + // this.via.x = via.x; + // this.via.y = via.y; + ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); + ctx.stroke(); + //ctx.circle(via.x,via.y,2) + //ctx.stroke(); + return via; + } + } + else { + ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); + ctx.stroke(); + return this.via; + } + } + else { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; } - - return img; }; - module.exports = Images; - - -/***/ }, -/* 56 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ + Edge.prototype._circle = function (ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + }; /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. + * Draw label with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + * @private */ - function Popup(container, x, y, text, style) { - if (container) { - this.container = container; - } - else { - this.container = document.body; - } + Edge.prototype._label = function (ctx, text, x, y) { + if (text) { + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.options.fontSize + "px " + this.options.fontFace; + var yLine; - // x, y and text are optional, see if a style object was passed in their place - if (style === undefined) { - if (typeof x === "object") { - style = x; - x = undefined; - } else if (typeof text === "object") { - style = text; - text = undefined; - } else { - // for backwards compatibility, in case clients other than Network are creating Popup directly - style = { - fontColor: 'black', - fontSize: 14, // px - fontFace: 'verdana', - color: { - border: '#666', - background: '#FFFFC6' - } + if (this.dirtyLabel == true) { + var lines = String(text).split('\n'); + var lineCount = lines.length; + var fontSize = Number(this.options.fontSize); + yLine = y + (1 - lineCount) / 2 * fontSize; + + var width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; } + var height = this.options.fontSize * lineCount; + var left = x - width / 2; + var top = y - height / 2; + + // cache + this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine}; } - } - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + var yLine = this.labelDimensions.yLine; + + ctx.save(); + + if (this.options.labelAlignment != "horizontal"){ + ctx.translate(x, yLine); + this._rotateForLabelAlignment(ctx); + x = 0; + yLine = 0; + } - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); + + this._drawLabelRect(ctx); + this._drawLabelText(ctx,x,yLine, lines, lineCount, fontSize); + + ctx.restore(); } - - // create the frame - this.frame = document.createElement('div'); - this.frame.className = 'network-tooltip'; - this.frame.style.color = style.fontColor; - this.frame.style.backgroundColor = style.color.background; - this.frame.style.borderColor = style.color.border; - this.frame.style.fontSize = style.fontSize + 'px'; - this.frame.style.fontFamily = style.fontFace; - this.container.appendChild(this.frame); - } + }; /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private */ - Popup.prototype.setPosition = function(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); + Edge.prototype._rotateForLabelAlignment = function(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); + + // rotate so label it is readable + if((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)){ + angleInDegrees = angleInDegrees + Math.PI; + } + + ctx.rotate(angleInDegrees); }; /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content + * Draws the label rectangle + * @param {CanvasRenderingContext2D} ctx + * @param {String} labelAlignment + * @private */ - Popup.prototype.setText = function(content) { - if (content instanceof Element) { - this.frame.innerHTML = ''; - this.frame.appendChild(content); - } - else { - this.frame.innerHTML = content; // string containing text or HTML + Edge.prototype._drawLabelRect = function(ctx) { + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; + + var lineMargin = 2; + + if (this.options.labelAlignment == 'line-center') { + ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); + } + else if (this.options.labelAlignment == 'line-above') { + ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); + } + else if (this.options.labelAlignment == 'line-below') { + ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); + } + else { + ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); + } } }; /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window + * Draws the label text + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} yLine + * @param {Array} lines + * @param {Number} lineCount + * @param {Number} fontSize + * @private */ - Popup.prototype.show = function (show) { - if (show === undefined) { - show = true; - } - - if (show) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSize) { + // draw text + ctx.fillStyle = this.options.fontColor || "black"; + ctx.textAlign = "center"; - var top = (this.y - height); - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; + // check for label alignment + if (this.options.labelAlignment != 'horizontal') { + var lineMargin = 2; + if (this.options.labelAlignment == 'line-above') { + ctx.textBaseline = "alphabetic"; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers } - - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; + else if (this.options.labelAlignment == 'line-below') { + ctx.textBaseline = "hanging"; + yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers } - if (left < this.padding) { - left = this.padding; + else { + ctx.textBaseline = "middle"; } - - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; } else { - this.hide(); + ctx.textBaseline = "middle"; } - }; - /** - * Hide the popup window - */ - Popup.prototype.hide = function () { - this.hidden = true; - this.frame.style.visibility = "hidden"; + // check for strokeWidth + if (this.options.fontStrokeWidth > 0){ + ctx.lineWidth = this.options.fontStrokeWidth; + ctx.strokeStyle = this.options.fontStrokeColor; + ctx.lineJoin = 'round'; + } + for (var i = 0; i < lineCount; i++) { + if(this.options.fontStrokeWidth > 0){ + ctx.strokeText(lines[i], x, yLine); + } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } }; - module.exports = Popup; - - -/***/ }, -/* 57 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private */ - function parseDOT (data) { - dot = data; - return parseGraph(); - } - - // token types enumeration - var TOKENTYPE = { - NULL : 0, - DELIMITER : 1, - IDENTIFIER: 2, - UNKNOWN : 3 - }; + Edge.prototype._drawDashLine = function(ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); - // map with all delimiters - var DELIMITERS = { - '{': true, - '}': true, - '[': true, - ']': true, - ';': true, - '=': true, - ',': true, + var via = null; + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.setLineDash !== undefined) { + ctx.save(); + // configure the dash pattern + var pattern = [0]; + if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { + pattern = [this.options.dash.length,this.options.dash.gap]; + } + else { + pattern = [5,5]; + } - '->': true, - '--': true - }; + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - var dot = ''; // current dot file - var index = 0; // current index in dot file - var c = ''; // current token character in expr - var token = ''; // current token - var tokenType = TOKENTYPE.NULL; // type of the token + // draw the line + via = this._line(ctx); - /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function first() { - index = 0; - c = dot.charAt(0); - } + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } + else { // unsupporting smooth lines + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]); + } + else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, + [this.options.dash.length,this.options.dash.gap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + } - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); - } + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); + point = {x:midpointX, y:midpointY}; + } + else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); + } + }; /** - * Preview the next character from the dot file. - * @return {String} cNext + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private */ - function nextPreview() { - return dot.charAt(index + 1); - } + Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } + }; /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); - } + Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } + }; /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a + * Redraw a edge as a line with an arrow halfway the line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private */ - function merge (a, b) { - if (!a) { - a = {}; - } + Edge.prototype._drawArrowCenter = function(ctx) { + var point; + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } - } - return a; - } + if (this.from != this.to) { + // draw line + var via = this._line(ctx); - /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value - */ - function setValue(obj, path, value) { - var keys = path.split('.'); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; - } - o = o[key]; + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + // draw an arrow halfway the line + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); + var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); + point = {x:midpointX, y:midpointY}; } else { - // this is the end point - o[key] = value; - } - } - } - - /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node - */ - function addNode(graph, node) { - var i, len; - var current = null; + point = this._pointOnLine(0.5); + } - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; - } + // draw label + if (this.label) { + this._label(ctx, this.label, point.x, point.y); } } - - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); + else { + // draw circle + var x, y; + var radius = 0.25 * Math.max(100,this.physics.springLength); + var node = this.from; + if (!node.width) { + node.resize(ctx); } - } - - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; - - if (!g.nodes) { - g.nodes = []; + if (node.width > node.height) { + x = node.x + node.width * 0.5; + y = node.y - radius; } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); + else { + x = node.x + radius; + y = node.y - node.height * 0.5; } - } + this._circle(ctx, x, y, radius); - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } - } + // draw all arrows + var angle = 0.2 * Math.PI; + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } } - } + }; - /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge - */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; + Edge.prototype._pointOnBezier = function(t) { + var via = this._getViaCoordinates(); - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes - } - edge.attr = merge(edge.attr || {}, attr); // merge attributes + var x = Math.pow(1-t,2)*this.from.x + (2*t*(1 - t))*via.x + Math.pow(t,2)*this.to.x; + var y = Math.pow(1-t,2)*this.from.y + (2*t*(1 - t))*via.y + Math.pow(t,2)*this.to.y; - return edge; + return {x:x,y:y}; } /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param from + * @param ctx + * @returns {*} + * @private */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ''; - - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); + Edge.prototype._findBorderPosition = function(from,ctx) { + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos,angle,distanceToBorder, distanceToNodes, difference; + var threshold = 0.2; + var node = this.to; + if (from == true) { + node = this.from; } - do { - var isComment = false; + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; - // skip comment - if (c == '#') { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { - i--; + pos = this._pointOnBezier(middle); + angle = Math.atan2((node.y - pos.y), (node.x - pos.x)); + distanceToBorder = node.distanceToBorder(ctx,angle); + distanceToNodes = Math.sqrt(Math.pow(pos.x-node.x,2) + Math.pow(pos.y-node.y,2)); + difference = distanceToBorder - distanceToNodes; + if (Math.abs(difference) < threshold) { + break; // found + } + else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (from == false) { + low = middle; } - if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { - // the # is at the start of a line, this is indeed a line comment - while (c != '' && c != '\n') { - next(); - } - isComment = true; + else { + high = middle; } } - if (c == '/' && nextPreview() == '/') { - // skip line comment - while (c != '' && c != '\n') { - next(); + else { + if (from == false) { + high = middle; } - isComment = true; - } - if (c == '/' && nextPreview() == '*') { - // skip block comment - while (c != '') { - if (c == '*' && nextPreview() == '/') { - // end of block comment found. skip these last two characters - next(); - next(); - break; - } - else { - next(); - } + else { + low = middle; } - isComment = true; } - // skip over whitespaces - while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter - next(); - } + iteration++; } - while (isComment); + pos.t = middle; - // check for end of dot file - if (c == '') { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; - } + return pos; + }; - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; - } + /** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawArrow = function(ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; - } + // set vars + var angle, length, arrowPos; - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == '-') { - token += c; - next(); + // if not connected to itself + if (this.from != this.to) { + // draw line + this._line(ctx); - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == 'false') { - token = false; // convert to boolean - } - else if (token == 'true') { - token = true; // convert to boolean + // draw arrow head + if (this.options.smoothCurves.enabled == true) { + var via = this._getViaCoordinates(); + arrowPos = this._findBorderPosition(false, ctx); + var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1)) + angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x)); } - else if (!isNaN(Number(token))) { - token = Number(token); // convert to number + else { + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + + arrowPos = {}; + arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } - // check for a string enclosed by double quotes - if (c == '"') { - next(); - while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { - token += c; - if (c == '"') { // skip the escape character - next(); + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrowPos.x,arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + point = this._pointOnBezier(0.5); } - next(); - } - if (c != '"') { - throw newSyntaxError('End of string " expected'); + else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; } + else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = 0.25 * Math.max(100,this.physics.springLength); + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width * 0.5; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; + } + else { + x = node.x + radius; + y = node.y - node.height * 0.5; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != '') { - token += c; - next(); + // draw all arrows + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } } - throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); - } + }; /** - * Parse a graph. - * @returns {Object} graph + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private */ - function parseGraph() { - var graph = {}; - - first(); - getToken(); - - // optional strict keyword - if (token == 'strict') { - graph.strict = true; - getToken(); + Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + if (this.options.smoothCurves.enabled == true) { + var xVia, yVia; + if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { + xVia = this.via.x; + yVia = this.via.y; + } + else { + var via = this._getViaCoordinates(); + xVia = via.x; + yVia = via.y; + } + var minDistance = 1e9; + var distance; + var i,t,x,y, lastX, lastY; + for (i = 0; i < 10; i++) { + t = 0.1*i; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; + if (i > 0) { + distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3); + minDistance = distance < minDistance ? distance : minDistance; + } + lastX = x; lastY = y; + } + returnValue = minDistance; + } + else { + returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3); + } } - - // graph or digraph keyword - if (token == 'graph' || token == 'digraph') { - graph.type = token; - getToken(); + else { + var x, y, dx, dy; + var radius = 0.25 * this.physics.springLength; + var node = this.from; + if (node.width > node.height) { + x = node.x + 0.5 * node.width; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - 0.5 * node.height; + } + dx = x - x3; + dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius); } - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); + if (this.labelDimensions.left < x3 && + this.labelDimensions.left + this.labelDimensions.width > x3 && + this.labelDimensions.top < y3 && + this.labelDimensions.top + this.labelDimensions.height > y3) { + return 0; } - - // open angle bracket - if (token != '{') { - throw newSyntaxError('Angle bracket { expected'); + else { + return returnValue; } - getToken(); + }; - // statements - parseStatements(graph); + Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) { + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); + if (u > 1) { + u = 1; } - getToken(); - - // end of file - if (token !== '') { - throw newSyntaxError('End of file expected'); + else if (u < 0) { + u = 0; } - getToken(); - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; - return graph; - } + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance - /** - * Parse a list with statements. - * @param {Object} graph - */ - function parseStatements (graph) { - while (token !== '' && token != '}') { - parseStatement(graph); - if (token == ';') { - getToken(); - } - } - } + return Math.sqrt(dx*dx + dy*dy); + }; /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph + * This allows the zoom level of the network to influence the rendering + * + * @param scale */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); + Edge.prototype.setScale = function(scale) { + this.networkScaleInv = 1.0/scale; + }; - return; - } - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + Edge.prototype.select = function() { + this.selected = true; + }; - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - var id = token; // id can be a string or a number - getToken(); + Edge.prototype.unselect = function() { + this.selected = false; + }; - if (token == '=') { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier expected'); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + Edge.prototype.positionBezierNode = function() { + if (this.via !== null && this.from !== null && this.to !== null) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); } - else { - parseNodeStatement(graph, id); + else if (this.via !== null) { + this.via.x = 0; + this.via.y = 0; } - } + }; /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph + * This function draws the control nodes for the manipulator. + * In order to enable this, only set the this.controlNodesEnabled to true. + * @param ctx */ - function parseSubgraph (graph) { - var subgraph = null; - - // optional subgraph keyword - if (token == 'subgraph') { - subgraph = {}; - subgraph.type = 'subgraph'; - getToken(); - - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); + Edge.prototype._drawControlNodes = function(ctx) { + if (this.controlNodesEnabled == true) { + if (this.controlNodes.from === null && this.controlNodes.to === null) { + var nodeIdFrom = "edgeIdFrom:".concat(this.id); + var nodeIdTo = "edgeIdTo:".concat(this.id); + var constants = { + nodes:{group:'', radius:7, borderWidth:2, borderWidthSelected: 2}, + physics:{damping:0}, + clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}} + }; + this.controlNodes.from = new Node( + {id:nodeIdFrom, + shape:'dot', + color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} + },{},{},constants); + this.controlNodes.to = new Node( + {id:nodeIdTo, + shape:'dot', + color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}} + },{},{},constants); } - } - - // open angle bracket - if (token == '{') { - getToken(); - if (!subgraph) { - subgraph = {}; + this.controlNodes.positions = {}; + if (this.controlNodes.from.selected == false) { + this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); + this.controlNodes.from.x = this.controlNodes.positions.from.x; + this.controlNodes.from.y = this.controlNodes.positions.from.y; } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; - - // statements - parseStatements(subgraph); - - // close angle bracket - if (token != '}') { - throw newSyntaxError('Angle bracket } expected'); + if (this.controlNodes.to.selected == false) { + this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); + this.controlNodes.to.x = this.controlNodes.positions.to.x; + this.controlNodes.to.y = this.controlNodes.positions.to.y; } - getToken(); - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; - - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); + this.controlNodes.from.draw(ctx); + this.controlNodes.to.draw(ctx); } - - return subgraph; - } + else { + this.controlNodes = {from:null, to:null, positions:{}}; + } + }; /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. + * Enable control nodes. + * @private */ - function parseAttributeStatement (graph) { - // attribute statements - if (token == 'node') { - getToken(); + Edge.prototype._enableControlNodes = function() { + this.fromBackup = this.from; + this.toBackup = this.to; + this.controlNodesEnabled = true; + }; - // node attributes - graph.node = parseAttributeList(); - return 'node'; + /** + * disable control nodes and remove from dynamicEdges from old node + * @private + */ + Edge.prototype._disableControlNodes = function() { + this.fromId = this.from.id; + this.toId = this.to.id; + if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges + this.fromBackup.detachEdge(this); } - else if (token == 'edge') { - getToken(); - - // edge attributes - graph.edge = parseAttributeList(); - return 'edge'; + else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges + this.toBackup.detachEdge(this); } - else if (token == 'graph') { - getToken(); - // graph attributes - graph.graph = parseAttributeList(); - return 'graph'; - } + this.fromBackup = null; + this.toBackup = null; + this.controlNodesEnabled = false; + }; - return null; - } /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id + * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. + * @param x + * @param y + * @returns {null} + * @private */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; + Edge.prototype._getSelectedControlNode = function(x,y) { + var positions = this.controlNodes.positions; + var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2)); + var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2)); + + if (fromDistance < 15) { + this.connectedNode = this.from; + this.from = this.controlNodes.from; + return this.controlNodes.from; } - addNode(graph, node); + else if (toDistance < 15) { + this.connectedNode = this.to; + this.to = this.controlNodes.to; + return this.controlNodes.to; + } + else { + return null; + } + }; - // edge statements - parseEdge(graph, id); - } /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node + * this resets the control nodes to their original position. + * @private */ - function parseEdge(graph, from) { - while (token == '->' || token == '--') { - var to; - var type = token; - getToken(); - - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } - else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Identifier or subgraph expected'); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } - - // parse edge attributes - var attr = parseAttributeList(); - - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); - - from = to; + Edge.prototype._restoreControlNodes = function() { + if (this.controlNodes.from.selected == true) { + this.from = this.connectedNode; + this.connectedNode = null; + this.controlNodes.from.unselect(); } - } + else if (this.controlNodes.to.selected == true) { + this.to = this.connectedNode; + this.connectedNode = null; + this.controlNodes.to.unselect(); + } + }; /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {x: *, y: *} */ - function parseAttributeList() { - var attr = null; - - while (token == '[') { - getToken(); - attr = {}; - while (token !== '' && token != ']') { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute name expected'); - } - var name = token; + Edge.prototype.getControlNodeFromPosition = function(ctx) { + // draw arrow head + var controlnodeFromPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeFromPos = this._findBorderPosition(true, ctx); + } + else { + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - getToken(); - if (token != '=') { - throw newSyntaxError('Equal sign = expected'); - } - getToken(); + var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); + var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; + controlnodeFromPos = {}; + controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; + controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + } - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError('Attribute value expected'); - } - var value = token; - setValue(attr, name, value); // name can be a path + return controlnodeFromPos; + }; - getToken(); - if (token ==',') { - getToken(); - } - } + /** + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} + */ + Edge.prototype.getControlNodeToPosition = function(ctx) { + // draw arrow head + var controlnodeFromPos,controlnodeToPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeToPos = this._findBorderPosition(false, ctx); + } + else { + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - if (token != ']') { - throw newSyntaxError('Bracket ] expected'); - } - getToken(); + controlnodeToPos = {}; + controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } - return attr; - } + return controlnodeToPos; + }; - /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err - */ - function newSyntaxError(message) { - return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); - } + module.exports = Edge; - /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} - */ - function chop (text, maxLength) { - return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); - } +/***/ }, +/* 58 */ +/***/ function(module, exports, __webpack_require__) { /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. */ - function forEach2(array1, array2, fn) { - if (Array.isArray(array1)) { - array1.forEach(function (elem1) { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } - else { - fn(elem1, array2); - } - }); + function Popup(container, x, y, text, style) { + if (container) { + this.container = container; } else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } - else { - fn(array1, array2); - } + this.container = document.body; } - } - - /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData - */ - function DOTToGraph (data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = 'image'; + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: 'black', + fontSize: 14, // px + fontFace: 'verdana', + color: { + border: '#666', + background: '#FFFFC6' + } } - graphData.nodes.push(graphNode); - }); - } - - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - var convertEdge = function (dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; - return graphEdge; } + } - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } - else { - from = { - id: dotEdge.from - } - } + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } - else { - to = { - id: dotEdge.to - } - } + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + // create the frame + this.frame = document.createElement('div'); + this.frame.className = 'network-tooltip'; + this.frame.style.color = style.fontColor; + this.frame.style.backgroundColor = style.color.background; + this.frame.style.borderColor = style.color.border; + this.frame.style.fontSize = style.fontSize + 'px'; + this.frame.style.fontFamily = style.fontFace; + this.container.appendChild(this.frame); + } - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); + /** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ + Popup.prototype.setPosition = function(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); + }; - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + Popup.prototype.setText = function(content) { + if (content instanceof Element) { + this.frame.innerHTML = ''; + this.frame.appendChild(content); } - - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + else { + this.frame.innerHTML = content; // string containing text or HTML } + }; - return graphData; - } - - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + /** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ + Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; + } + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; -/***/ }, -/* 58 */ -/***/ function(module, exports, __webpack_require__) { + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true - }, - nodes: { - allowedToMove: false, - parseColor: false + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; } - }; - if (options !== undefined) { - this.options.nodes['allowedToMove'] = options.allowedToMove | false; - this.options.nodes['parseColor'] = options.parseColor | false; - this.options.edges['inheritColor'] = options.inheritColor | true; + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + this.hidden = false; } - - var gEdges = gephiJSON.edges; - var gNodes = gephiJSON.nodes; - for (var i = 0; i < gEdges.length; i++) { - var edge = {}; - var gEdge = gEdges[i]; - edge['id'] = gEdge.id; - edge['from'] = gEdge.source; - edge['to'] = gEdge.target; - edge['attributes'] = gEdge.attributes; - // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; - // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; - edge['color'] = gEdge.color; - edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor; - edges.push(edge); + else { + this.hide(); } + }; - for (var i = 0; i < gNodes.length; i++) { - var node = {}; - var gNode = gNodes[i]; - node['id'] = gNode.id; - node['attributes'] = gNode.attributes; - node['x'] = gNode.x; - node['y'] = gNode.y; - node['label'] = gNode.label; - if (this.options.nodes.parseColor == true) { - node['color'] = gNode.color; - } - else { - node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined; - } - node['radius'] = gNode.size; - node['allowedToMoveX'] = this.options.nodes.allowedToMove; - node['allowedToMoveY'] = this.options.nodes.allowedToMove; - nodes.push(node); - } + /** + * Hide the popup window + */ + Popup.prototype.hide = function () { + this.hidden = true; + this.frame.style.visibility = "hidden"; + }; - return {nodes:nodes, edges:edges}; - } + module.exports = Popup; - exports.parseGephi = parseGephi; /***/ }, /* 59 */ /***/ function(module, exports, __webpack_require__) { - var PhysicsMixin = __webpack_require__(62); - var ClusterMixin = __webpack_require__(63); - var SectorsMixin = __webpack_require__(64); - var SelectionMixin = __webpack_require__(65); - var ManipulationMixin = __webpack_require__(66); - var NavigationMixin = __webpack_require__(67); - var HierarchicalLayoutMixin = __webpack_require__(68); + var PhysicsMixin = __webpack_require__(60); + var ClusterMixin = __webpack_require__(64); + var SectorsMixin = __webpack_require__(65); + var SelectionMixin = __webpack_require__(66); + var ManipulationMixin = __webpack_require__(67); + var NavigationMixin = __webpack_require__(68); + var HierarchicalLayoutMixin = __webpack_require__(69); /** * Load a mixin into the network object @@ -29778,478 +29865,885 @@ return /******/ (function(modules) { // webpackBootstrap /* 60 */ /***/ function(module, exports, __webpack_require__) { - // English - exports['en'] = { - edit: 'Edit', - del: 'Delete selected', - back: 'Back', - addNode: 'Add Node', - addEdge: 'Add Edge', - editNode: 'Edit Node', - editEdge: 'Edit Edge', - addDescription: 'Click in an empty space to place a new node.', - edgeDescription: 'Click on a node and drag the edge to another node to connect them.', - editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.', - createEdgeError: 'Cannot link edges to a cluster.', - deleteClusterError: 'Clusters cannot be deleted.' + var util = __webpack_require__(1); + var RepulsionMixin = __webpack_require__(61); + var HierarchialRepulsionMixin = __webpack_require__(62); + var BarnesHutMixin = __webpack_require__(63); + + /** + * Toggling barnes Hut calculation on and off. + * + * @private + */ + exports._toggleBarnesHut = function () { + this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; + this._loadSelectedForceSolver(); + this.moving = true; + this.start(); }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; - // Dutch - exports['nl'] = { - edit: 'Wijzigen', - del: 'Selectie verwijderen', - back: 'Terug', - addNode: 'Node toevoegen', - addEdge: 'Link toevoegen', - editNode: 'Node wijzigen', - editEdge: 'Link wijzigen', - addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.', - edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.', - editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.', - createEdgeError: 'Kan geen link maken naar een cluster.', - deleteClusterError: 'Clusters kunnen niet worden verwijderd.' + + /** + * This loads the node force solver based on the barnes hut or repulsion algorithm + * + * @private + */ + exports._loadSelectedForceSolver = function () { + // this overloads the this._calculateNodeForces + if (this.constants.physics.barnesHut.enabled == true) { + this._clearMixin(RepulsionMixin); + this._clearMixin(HierarchialRepulsionMixin); + + this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; + this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; + this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; + this.constants.physics.damping = this.constants.physics.barnesHut.damping; + + this._loadMixin(BarnesHutMixin); + } + else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._clearMixin(BarnesHutMixin); + this._clearMixin(RepulsionMixin); + + this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; + this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; + this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; + this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; + + this._loadMixin(HierarchialRepulsionMixin); + } + else { + this._clearMixin(BarnesHutMixin); + this._clearMixin(HierarchialRepulsionMixin); + this.barnesHutTree = undefined; + + this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; + this.constants.physics.springLength = this.constants.physics.repulsion.springLength; + this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; + this.constants.physics.damping = this.constants.physics.repulsion.damping; + + this._loadMixin(RepulsionMixin); + } }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; + /** + * Before calculating the forces, we check if we need to cluster to keep up performance and we check + * if there is more than one node. If it is just one node, we dont calculate anything. + * + * @private + */ + exports._initializeForceCalculation = function () { + // stop calculation if there is only one node + if (this.nodeIndices.length == 1) { + this.nodes[this.nodeIndices[0]]._setForce(0, 0); + } + else { + // if there are too many nodes on screen, we cluster without repositioning + if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { + this.clusterToFit(this.constants.clustering.reduceToNodes, false); + } + + // we now start the force calculation + this._calculateForces(); + } + }; -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { /** - * Canvas shapes used by Network + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + * @private */ - if (typeof CanvasRenderingContext2D !== 'undefined') { + exports._calculateForces = function () { + // Gravity is required to keep separated groups from floating off + // the forces are reset to zero in this loop by using _setForce instead + // of _addForce - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function(x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2*Math.PI, false); - }; + this._calculateGravitationalForces(); + this._calculateNodeForces(); - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function(x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; + if (this.constants.physics.springConstant > 0) { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this._calculateSpringForcesWithSupport(); + } + else { + if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._calculateHierarchicalSpringForces(); + } + else { + this._calculateSpringForces(); + } + } + } + }; - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.nodes with the support nodes. + * + * @private + */ + exports._updateCalculationNodes = function () { + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this.calculationNodes = {}; + this.calculationNodeIndices = []; - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); - }; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.calculationNodes[nodeId] = this.nodes[nodeId]; + } + } + var supportNodes = this.sectors['support']['nodes']; + for (var supportNodeId in supportNodes) { + if (supportNodes.hasOwnProperty(supportNodeId)) { + if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { + this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } + else { + supportNodes[supportNodeId]._setForce(0, 0); + } + } + } - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); + for (var idx in this.calculationNodes) { + if (this.calculationNodes.hasOwnProperty(idx)) { + this.calculationNodeIndices.push(idx); + } + } + } + else { + this.calculationNodes = this.nodes; + this.calculationNodeIndices = this.nodeIndices; + } + }; - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; + /** + * this function applies the central gravity effect to keep groups from floating off + * + * @private + */ + exports._calculateGravitationalForces = function () { + var dx, dy, distance, node, i; + var nodes = this.calculationNodes; + var gravity = this.constants.physics.centralGravity; + var gravityForce = 0; - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function(x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); + for (i = 0; i < this.calculationNodeIndices.length; i++) { + node = nodes[this.calculationNodeIndices[i]]; + node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default" && gravity != 0) { + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); - for (var n = 0; n < 10; n++) { - var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; - this.lineTo( - x + radius * Math.sin(n * 2 * Math.PI / 10), - y - radius * Math.cos(n * 2 * Math.PI / 10) - ); + gravityForce = (distance == 0) ? 0 : (gravity / distance); + node.fx = dx * gravityForce; + node.fy = dy * gravityForce; + } + else { + node.fx = 0; + node.fy = 0; } + } + }; - this.closePath(); - }; - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { - var r2d = Math.PI/180; - if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x - if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x+r,y); - this.lineTo(x+w-r,y); - this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); - this.lineTo(x+w,y+h-r); - this.arc(x+w-r,y+h-r,r,0,r2d*90,false); - this.lineTo(x+r,y+h); - this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); - this.lineTo(x,y+r); - this.arc(x+r,y+r,r,r2d*180,r2d*270,false); - }; - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { - var kappa = .5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - this.beginPath(); - this.moveTo(x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - }; + /** + * this function calculates the effects of the springs in the case of unsmooth curves. + * + * @private + */ + exports._calculateSpringForces = function () { + var edgeLength, edge, edgeId; + var dx, dy, fx, fy, springForce, distance; + var edges = this.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + edgeLength = edge.physics.springLength; + // this implies that the edges between big clusters are longer + edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; + + dx = (edge.from.x - edge.to.x); + dy = (edge.from.y - edge.to.y); + distance = Math.sqrt(dx * dx + dy * dy); + + if (distance == 0) { + distance = 0.01; + } + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + edge.from.fx += fx; + edge.from.fy += fy; + edge.to.fx -= fx; + edge.to.fy -= fy; + } + } + } + } + }; + + + + + /** + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private + */ + exports._calculateSpringForcesWithSupport = function () { + var edgeLength, edge, edgeId, combinedClusterSize; + var edges = this.edges; + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + if (edge.via != null) { + var node1 = edge.to; + var node2 = edge.via; + var node3 = edge.from; + + edgeLength = edge.physics.springLength; + + combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; + + // this implies that the edges between big clusters are longer + edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } + } + } + } + } + }; + /** + * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + exports._calculateSpringForce = function (node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { - var f = 1/3; - var wEllipse = w; - var hEllipse = h * f; + dx = (node1.x - node2.x); + dy = (node1.y - node2.y); + distance = Math.sqrt(dx * dx + dy * dy); - var kappa = .5522848, - ox = (wEllipse / 2) * kappa, // control point offset horizontal - oy = (hEllipse / 2) * kappa, // control point offset vertical - xe = x + wEllipse, // x-end - ye = y + hEllipse, // y-end - xm = x + wEllipse / 2, // x-middle - ym = y + hEllipse / 2, // y-middle - ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse - yeb = y + h; // y-end, bottom ellipse + if (distance == 0) { + distance = 0.01; + } - this.beginPath(); - this.moveTo(xe, ym); + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + fx = dx * springForce; + fy = dy * springForce; - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + node1.fx += fx; + node1.fy += fy; + node2.fx -= fx; + node2.fy -= fy; + }; - this.lineTo(xe, ymb); - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + exports._cleanupPhysicsConfiguration = function() { + if (this.physicsConfiguration !== undefined) { + while (this.physicsConfiguration.hasChildNodes()) { + this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); + } - this.lineTo(x, ym); - }; + this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); + this.physicsConfiguration = undefined; + } + } + /** + * Load the HTML for the physics config and bind it + * @private + */ + exports._loadPhysicsConfiguration = function () { + if (this.physicsConfiguration === undefined) { + this.backupConstants = {}; + util.deepExtend(this.backupConstants,this.constants); - /** - * Draw an arrow point (no line) - */ - CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { - // tail - var xt = x - length * Math.cos(angle); - var yt = y - length * Math.sin(angle); + var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); + var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); + var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; + this.physicsConfiguration = document.createElement('div'); + this.physicsConfiguration.className = "PhysicsConfiguration"; + this.physicsConfiguration.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Options:
' + this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); + this.optionsDiv = document.createElement("div"); + this.optionsDiv.style.fontSize = "14px"; + this.optionsDiv.style.fontFamily = "verdana"; + this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); - // left - var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); - var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + var rangeElement; + rangeElement = document.getElementById('graph_BH_gc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); + rangeElement = document.getElementById('graph_BH_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_BH_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_BH_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_BH_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + rangeElement = document.getElementById('graph_R_nd'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); + rangeElement = document.getElementById('graph_R_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_R_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_R_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_R_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + rangeElement = document.getElementById('graph_H_nd'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); + rangeElement = document.getElementById('graph_H_cg'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); + rangeElement = document.getElementById('graph_H_sc'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); + rangeElement = document.getElementById('graph_H_sl'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); + rangeElement = document.getElementById('graph_H_damp'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); + rangeElement = document.getElementById('graph_H_direction'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); + rangeElement = document.getElementById('graph_H_levsep'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); + rangeElement = document.getElementById('graph_H_nspac'); + rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); - /** - * Sets up the dashedLine functionality for drawing - * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - * @author David Jordan - * @date 2012-08-08 - */ - CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ - if (!dashArray) dashArray=[10,5]; - if (dashLength==0) dashLength = 0.001; // Hack for Safari - var dashCount = dashArray.length; - this.moveTo(x, y); - var dx = (x2-x), dy = (y2-y); - var slope = dy/dx; - var distRemaining = Math.sqrt( dx*dx + dy*dy ); - var dashIndex=0, draw=true; - while (distRemaining>=0.1){ - var dashLength = dashArray[dashIndex++%dashCount]; - if (dashLength > distRemaining) dashLength = distRemaining; - var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); - if (dx<0) xStep = -xStep; - x += xStep; - y += slope*xStep; - this[draw ? 'lineTo' : 'moveTo'](x,y); - distRemaining -= dashLength; - draw = !draw; + var radioButton1 = document.getElementById("graph_physicsMethod1"); + var radioButton2 = document.getElementById("graph_physicsMethod2"); + var radioButton3 = document.getElementById("graph_physicsMethod3"); + radioButton2.checked = true; + if (this.constants.physics.barnesHut.enabled) { + radioButton1.checked = true; + } + if (this.constants.hierarchicalLayout.enabled) { + radioButton3.checked = true; } - }; - // TODO: add diamond shape - } + var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); + var graph_repositionNodes = document.getElementById("graph_repositionNodes"); + var graph_generateOptions = document.getElementById("graph_generateOptions"); + + graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); + graph_repositionNodes.onclick = graphRepositionNodes.bind(this); + graph_generateOptions.onclick = graphGenerateOptions.bind(this); + if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { + graph_toggleSmooth.style.background = "#A4FF56"; + } + else { + graph_toggleSmooth.style.background = "#FF8532"; + } -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { + switchConfigurations.apply(this); - var util = __webpack_require__(1); - var RepulsionMixin = __webpack_require__(69); - var HierarchialRepulsionMixin = __webpack_require__(70); - var BarnesHutMixin = __webpack_require__(71); + radioButton1.onchange = switchConfigurations.bind(this); + radioButton2.onchange = switchConfigurations.bind(this); + radioButton3.onchange = switchConfigurations.bind(this); + } + }; /** - * Toggling barnes Hut calculation on and off. + * This overwrites the this.constants. * + * @param constantsVariableName + * @param value * @private */ - exports._toggleBarnesHut = function () { - this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; - this._loadSelectedForceSolver(); - this.moving = true; - this.start(); + exports._overWriteGraphConstants = function (constantsVariableName, value) { + var nameArray = constantsVariableName.split("_"); + if (nameArray.length == 1) { + this.constants[nameArray[0]] = value; + } + else if (nameArray.length == 2) { + this.constants[nameArray[0]][nameArray[1]] = value; + } + else if (nameArray.length == 3) { + this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; + } }; /** - * This loads the node force solver based on the barnes hut or repulsion algorithm - * - * @private + * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. */ - exports._loadSelectedForceSolver = function () { - // this overloads the this._calculateNodeForces - if (this.constants.physics.barnesHut.enabled == true) { - this._clearMixin(RepulsionMixin); - this._clearMixin(HierarchialRepulsionMixin); + function graphToggleSmoothCurves () { + this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; + var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); + if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} + else {graph_toggleSmooth.style.background = "#FF8532";} - this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; - this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; - this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; - this.constants.physics.damping = this.constants.physics.barnesHut.damping; + this._configureSmoothCurves(false); + } - this._loadMixin(BarnesHutMixin); + /** + * this function is used to scramble the nodes + * + */ + function graphRepositionNodes () { + for (var nodeId in this.calculationNodes) { + if (this.calculationNodes.hasOwnProperty(nodeId)) { + this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; + this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; + } } - else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._clearMixin(BarnesHutMixin); - this._clearMixin(RepulsionMixin); - - this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; - this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; + if (this.constants.hierarchicalLayout.enabled == true) { + this._setupHierarchicalLayout(); + showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); + showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); + showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); + showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); + showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); + } + else { + this.repositionNodes(); + } + this.moving = true; + this.start(); + } - this._loadMixin(HierarchialRepulsionMixin); + /** + * this is used to generate an options file from the playing with physics system. + */ + function graphGenerateOptions () { + var options = "No options are required, default values used."; + var optionsSpecific = []; + var radioButton1 = document.getElementById("graph_physicsMethod1"); + var radioButton2 = document.getElementById("graph_physicsMethod2"); + if (radioButton1.checked == true) { + if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options = "var options = {"; + options += "physics: {barnesHut: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " + } + } + options += '}}' + } + if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { + if (optionsSpecific.length == 0) {options = "var options = {";} + else {options += ", "} + options += "smoothCurves: " + this.constants.smoothCurves.enabled; + } + if (options != "No options are required, default values used.") { + options += '};' + } + } + else if (radioButton2.checked == true) { + options = "var options = {"; + options += "physics: {barnesHut: {enabled: false}"; + if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options += ", repulsion: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " + } + } + options += '}}' + } + if (optionsSpecific.length == 0) {options += "}"} + if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { + options += ", smoothCurves: " + this.constants.smoothCurves; + } + options += '};' } else { - this._clearMixin(BarnesHutMixin); - this._clearMixin(HierarchialRepulsionMixin); - this.barnesHutTree = undefined; + options = "var options = {"; + if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} + if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} + if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} + if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} + if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} + if (optionsSpecific.length != 0) { + options += "physics: {hierarchicalRepulsion: {"; + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", "; + } + } + options += '}},'; + } + options += 'hierarchicalLayout: {'; + optionsSpecific = []; + if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} + if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} + if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} + if (optionsSpecific.length != 0) { + for (var i = 0; i < optionsSpecific.length; i++) { + options += optionsSpecific[i]; + if (i < optionsSpecific.length - 1) { + options += ", " + } + } + options += '}' + } + else { + options += "enabled:true}"; + } + options += '};' + } - this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; - this.constants.physics.springLength = this.constants.physics.repulsion.springLength; - this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; - this.constants.physics.damping = this.constants.physics.repulsion.damping; - this._loadMixin(RepulsionMixin); - } - }; + this.optionsDiv.innerHTML = options; + } /** - * Before calculating the forces, we check if we need to cluster to keep up performance and we check - * if there is more than one node. If it is just one node, we dont calculate anything. + * this is used to switch between barnesHut, repulsion and hierarchical. + * + */ + function switchConfigurations () { + var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; + var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; + var tableId = "graph_" + radioButton + "_table"; + var table = document.getElementById(tableId); + table.style.display = "block"; + for (var i = 0; i < ids.length; i++) { + if (ids[i] != tableId) { + table = document.getElementById(ids[i]); + table.style.display = "none"; + } + } + this._restoreNodes(); + if (radioButton == "R") { + this.constants.hierarchicalLayout.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = false; + this.constants.physics.barnesHut.enabled = false; + } + else if (radioButton == "H") { + if (this.constants.hierarchicalLayout.enabled == false) { + this.constants.hierarchicalLayout.enabled = true; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this.constants.physics.barnesHut.enabled = false; + this.constants.smoothCurves.enabled = false; + this._setupHierarchicalLayout(); + } + } + else { + this.constants.hierarchicalLayout.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = false; + this.constants.physics.barnesHut.enabled = true; + } + this._loadSelectedForceSolver(); + var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); + if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} + else {graph_toggleSmooth.style.background = "#FF8532";} + this.moving = true; + this.start(); + } + + + /** + * this generates the ranges depending on the iniital values. * - * @private + * @param id + * @param map + * @param constantsVariableName */ - exports._initializeForceCalculation = function () { - // stop calculation if there is only one node - if (this.nodeIndices.length == 1) { - this.nodes[this.nodeIndices[0]]._setForce(0, 0); + function showValueOfRange (id,map,constantsVariableName) { + var valueId = id + "_value"; + var rangeValue = document.getElementById(id).value; + + if (Array.isArray(map)) { + document.getElementById(valueId).value = map[parseInt(rangeValue)]; + this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); } else { - // if there are too many nodes on screen, we cluster without repositioning - if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToNodes, false); - } - - // we now start the force calculation - this._calculateForces(); + document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); + this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); } - }; + if (constantsVariableName == "hierarchicalLayout_direction" || + constantsVariableName == "hierarchicalLayout_levelSeparation" || + constantsVariableName == "hierarchicalLayout_nodeSpacing") { + this._setupHierarchicalLayout(); + } + this.moving = true; + this.start(); + } - /** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity - * @private - */ - exports._calculateForces = function () { - // Gravity is required to keep separated groups from floating off - // the forces are reset to zero in this loop by using _setForce instead - // of _addForce - this._calculateGravitationalForces(); - this._calculateNodeForces(); - if (this.constants.physics.springConstant > 0) { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._calculateSpringForcesWithSupport(); - } - else { - if (this.constants.physics.hierarchicalRepulsion.enabled == true) { - this._calculateHierarchicalSpringForces(); - } - else { - this._calculateSpringForces(); - } - } - } - }; +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.nodes with the support nodes. + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. * * @private */ - exports._updateCalculationNodes = function () { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this.calculationNodes = {}; - this.calculationNodeIndices = []; + exports._calculateNodeForces = function () { + var dx, dy, angle, distance, fx, fy, combinedClusterSize, + repulsingForce, node1, node2, i, j; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId] = this.nodes[nodeId]; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + // approximation constants + var a_base = -2 / 3; + var b = 4 / 3; + + // repulsing forces between nodes + var nodeDistance = this.constants.physics.repulsion.nodeDistance; + var minimumDistance = nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // same condition as BarnesHut, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1*Math.random(); + dx = distance; } - } - var supportNodes = this.sectors['support']['nodes']; - for (var supportNodeId in supportNodes) { - if (supportNodes.hasOwnProperty(supportNodeId)) { - if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { - this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + + minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); + var a = a_base / minimumDistance; + if (distance < 2 * minimumDistance) { + if (distance < 0.5 * minimumDistance) { + repulsingForce = 1.0; } else { - supportNodes[supportNodeId]._setForce(0, 0); + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) } - } - } - for (var idx in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(idx)) { - this.calculationNodeIndices.push(idx); + // amplify the repulsion for clusters. + repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; + repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + node1.fx -= fx; + node1.fy -= fy; + node2.fx += fx; + node2.fy += fy; + } } } - else { - this.calculationNodes = this.nodes; - this.calculationNodeIndices = this.nodeIndices; - } }; +/***/ }, +/* 62 */ +/***/ function(module, exports, __webpack_require__) { + /** - * this function applies the central gravity effect to keep groups from floating off + * Calculate the forces the nodes apply on eachother based on a repulsion field. + * This field is linearly approximated. * * @private */ - exports._calculateGravitationalForces = function () { - var dx, dy, distance, node, i; + exports._calculateNodeForces = function () { + var dx, dy, distance, fx, fy, + repulsingForce, node1, node2, i, j; + var nodes = this.calculationNodes; - var gravity = this.constants.physics.centralGravity; - var gravityForce = 0; + var nodeIndices = this.calculationNodeIndices; - for (i = 0; i < this.calculationNodeIndices.length; i++) { - node = nodes[this.calculationNodeIndices[i]]; - node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default" && gravity != 0) { - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); + // repulsing forces between nodes + var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; - gravityForce = (distance == 0) ? 0 : (gravity / distance); - node.fx = dx * gravityForce; - node.fy = dy * gravityForce; - } - else { - node.fx = 0; - node.fy = 0; + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + // nodes only affect nodes on their level + if (node1.level == node2.level) { + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); + } + else { + repulsingForce = 0; + } + // normalize force with + if (distance == 0) { + distance = 0.01; + } + else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + node1.fx -= fx; + node1.fy -= fy; + node2.fx += fx; + node2.fy += fy; + } } } }; - - /** * this function calculates the effects of the springs in the case of unsmooth curves. * * @private */ - exports._calculateSpringForces = function () { + exports._calculateHierarchicalSpringForces = function () { var edgeLength, edge, edgeId; var dx, dy, fx, fy, springForce, distance; var edges = this.edges; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + + for (var i = 0; i < nodeIndices.length; i++) { + var node1 = nodes[nodeIndices[i]]; + node1.springFx = 0; + node1.springFy = 0; + } + + // forces caused by the edges, modelled as springs for (edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { @@ -30275,509 +30769,464 @@ return /******/ (function(modules) { // webpackBootstrap fx = dx * springForce; fy = dy * springForce; - edge.from.fx += fx; - edge.from.fy += fy; - edge.to.fx -= fx; - edge.to.fy -= fy; + + + if (edge.to.level != edge.from.level) { + edge.to.springFx -= fx; + edge.to.springFy -= fy; + edge.from.springFx += fx; + edge.from.springFy += fy; + } + else { + var factor = 0.5; + edge.to.fx -= factor*fx; + edge.to.fy -= factor*fy; + edge.from.fx += factor*fx; + edge.from.fy += factor*fy; + } } } } } - }; - - - - - /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * - * @private - */ - exports._calculateSpringForcesWithSupport = function () { - var edgeLength, edge, edgeId, combinedClusterSize; - var edges = this.edges; - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - if (edge.via != null) { - var node1 = edge.to; - var node2 = edge.via; - var node3 = edge.from; + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); + springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); - edgeLength = edge.physics.springLength; + node.fx += springFx; + node.fy += springFy; + } - combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + totalFx += node.fx; + totalFy += node.fy; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; - // this implies that the edges between big clusters are longer - edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } - } - } - } + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + node.fx -= correctionFx; + node.fy -= correctionFy; } + }; +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { /** - * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. * - * @param node1 - * @param node2 - * @param edgeLength * @private */ - exports._calculateSpringForce = function (node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = (node1.x - node2.x); - dy = (node1.y - node2.y); - distance = Math.sqrt(dx * dx + dy * dy); - - if (distance == 0) { - distance = 0.01; - } - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; + exports._calculateNodeForces = function() { + if (this.constants.physics.barnesHut.gravitationalConstant != 0) { + var node; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + var nodeCount = nodeIndices.length; - node1.fx += fx; - node1.fy += fy; - node2.fx -= fx; - node2.fy -= fy; - }; + this._formBarnesHutTree(nodes,nodeIndices); + var barnesHutTree = this.barnesHutTree; - exports._cleanupPhysicsConfiguration = function() { - if (this.physicsConfiguration !== undefined) { - while (this.physicsConfiguration.hasChildNodes()) { - this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHut condition + this._getForceContribution(barnesHutTree.root.children.NW,node); + this._getForceContribution(barnesHutTree.root.children.NE,node); + this._getForceContribution(barnesHutTree.root.children.SW,node); + this._getForceContribution(barnesHutTree.root.children.SE,node); + } } - - this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); - this.physicsConfiguration = undefined; } - } + }; + /** - * Load the HTML for the physics config and bind it + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node * @private */ - exports._loadPhysicsConfiguration = function () { - if (this.physicsConfiguration === undefined) { - this.backupConstants = {}; - util.deepExtend(this.backupConstants,this.constants); - - var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); - var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) - - var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; - this.physicsConfiguration = document.createElement('div'); - this.physicsConfiguration.className = "PhysicsConfiguration"; - this.physicsConfiguration.innerHTML = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Options:
' - this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); - this.optionsDiv = document.createElement("div"); - this.optionsDiv.style.fontSize = "14px"; - this.optionsDiv.style.fontFamily = "verdana"; - this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); - - var rangeElement; - rangeElement = document.getElementById('graph_BH_gc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); - rangeElement = document.getElementById('graph_BH_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_BH_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_BH_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_BH_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_R_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); - rangeElement = document.getElementById('graph_R_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_R_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_R_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_R_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); - - rangeElement = document.getElementById('graph_H_nd'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - rangeElement = document.getElementById('graph_H_cg'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); - rangeElement = document.getElementById('graph_H_sc'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); - rangeElement = document.getElementById('graph_H_sl'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); - rangeElement = document.getElementById('graph_H_damp'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); - rangeElement = document.getElementById('graph_H_direction'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); - rangeElement = document.getElementById('graph_H_levsep'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); - rangeElement = document.getElementById('graph_H_nspac'); - rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); - - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - var radioButton3 = document.getElementById("graph_physicsMethod3"); - radioButton2.checked = true; - if (this.constants.physics.barnesHut.enabled) { - radioButton1.checked = true; - } - if (this.constants.hierarchicalLayout.enabled) { - radioButton3.checked = true; - } + exports._getForceContribution = function(parentBranch,node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx,dy,distance; - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - var graph_repositionNodes = document.getElementById("graph_repositionNodes"); - var graph_generateOptions = document.getElementById("graph_generateOptions"); + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); - graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); - graph_repositionNodes.onclick = graphRepositionNodes.bind(this); - graph_generateOptions.onclick = graphGenerateOptions.bind(this); - if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { - graph_toggleSmooth.style.background = "#A4FF56"; + // BarnesHut condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1*Math.random(); + dx = distance; + } + var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; } else { - graph_toggleSmooth.style.background = "#FF8532"; + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW,node); + this._getForceContribution(parentBranch.children.NE,node); + this._getForceContribution(parentBranch.children.SW,node); + this._getForceContribution(parentBranch.children.SE,node); + } + else { // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5*Math.random(); + dx = distance; + } + var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; + } + } } - - - switchConfigurations.apply(this); - - radioButton1.onchange = switchConfigurations.bind(this); - radioButton2.onchange = switchConfigurations.bind(this); - radioButton3.onchange = switchConfigurations.bind(this); } }; /** - * This overwrites the this.constants. + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. * - * @param constantsVariableName - * @param value + * @param nodes + * @param nodeIndices * @private */ - exports._overWriteGraphConstants = function (constantsVariableName, value) { - var nameArray = constantsVariableName.split("_"); - if (nameArray.length == 1) { - this.constants[nameArray[0]] = value; - } - else if (nameArray.length == 2) { - this.constants[nameArray[0]][nameArray[1]] = value; + exports._formBarnesHutTree = function(nodes,nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX =-Number.MAX_VALUE, + maxY =-Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { minX = x; } + if (x > maxX) { maxX = x; } + if (y < minY) { minY = y; } + if (y > maxY) { maxY = y; } + } } - else if (nameArray.length == 3) { - this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize + else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize + + + var minimumTreeSize = 1e-5; + var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = { + root:{ + centerOfMass: {x:0, y:0}, + mass:0, + range: { + minX: centerX-halfRootSize,maxX:centerX+halfRootSize, + minY: centerY-halfRootSize,maxY:centerY+halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data:null}, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root,node); + } } + + // make global + this.barnesHutTree = barnesHutTree }; /** - * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private */ - function graphToggleSmoothCurves () { - this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} + exports._updateBranchMass = function(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1/totalMass; + + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); + parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; + + }; - this._configureSmoothCurves(false); - } /** - * this function is used to scramble the nodes + * determine in which branch the node will be placed. * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private */ - function graphRepositionNodes () { - for (var nodeId in this.calculationNodes) { - if (this.calculationNodes.hasOwnProperty(nodeId)) { - this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; - this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; - } - } - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); - showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); - showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); - showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); - showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); - } - else { - this.repositionNodes(); + exports._placeInTree = function(parentBranch,node,skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch,node); } - this.moving = true; - this.start(); - } - /** - * this is used to generate an options file from the playing with physics system. - */ - function graphGenerateOptions () { - var options = "No options are required, default values used."; - var optionsSpecific = []; - var radioButton1 = document.getElementById("graph_physicsMethod1"); - var radioButton2 = document.getElementById("graph_physicsMethod2"); - if (radioButton1.checked == true) { - if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options = "var options = {"; - options += "physics: {barnesHut: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' - } - if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { - if (optionsSpecific.length == 0) {options = "var options = {";} - else {options += ", "} - options += "smoothCurves: " + this.constants.smoothCurves.enabled; + if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { // in NW + this._placeInRegion(parentBranch,node,"NW"); } - if (options != "No options are required, default values used.") { - options += '};' + else { // in SW + this._placeInRegion(parentBranch,node,"SW"); } } - else if (radioButton2.checked == true) { - options = "var options = {"; - options += "physics: {barnesHut: {enabled: false}"; - if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += ", repulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } - } - options += '}}' + else { // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { // in NE + this._placeInRegion(parentBranch,node,"NE"); } - if (optionsSpecific.length == 0) {options += "}"} - if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { - options += ", smoothCurves: " + this.constants.smoothCurves; + else { // in SE + this._placeInRegion(parentBranch,node,"SE"); } - options += '};' } - else { - options = "var options = {"; - if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} - if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} - if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} - if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} - if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} - if (optionsSpecific.length != 0) { - options += "physics: {hierarchicalRepulsion: {"; - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", "; - } + }; + + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + exports._placeInRegion = function(parentBranch,node,region) { + switch (parentBranch.children[region].childrenCount) { + case 0: // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region],node); + break; + case 1: // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && + parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); } - options += '}},'; - } - options += 'hierarchicalLayout: {'; - optionsSpecific = []; - if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} - if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} - if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} - if (optionsSpecific.length != 0) { - for (var i = 0; i < optionsSpecific.length; i++) { - options += optionsSpecific[i]; - if (i < optionsSpecific.length - 1) { - options += ", " - } + else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region],node); } - options += '}' - } - else { - options += "enabled:true}"; - } - options += '};' + break; + case 4: // place in branch + this._placeInTree(parentBranch.children[region],node); + break; } + }; - this.optionsDiv.innerHTML = options; - } - /** - * this is used to switch between barnesHut, repulsion and hierarchical. + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. * + * @param parentBranch + * @private */ - function switchConfigurations () { - var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; - var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; - var tableId = "graph_" + radioButton + "_table"; - var table = document.getElementById(tableId); - table.style.display = "block"; - for (var i = 0; i < ids.length; i++) { - if (ids[i] != tableId) { - table = document.getElementById(ids[i]); - table.style.display = "none"; - } - } - this._restoreNodes(); - if (radioButton == "R") { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = false; + exports._splitBranch = function(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; } - else if (radioButton == "H") { - if (this.constants.hierarchicalLayout.enabled == false) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - this.constants.smoothCurves.enabled = false; - this._setupHierarchicalLayout(); - } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch,"NW"); + this._insertRegion(parentBranch,"NE"); + this._insertRegion(parentBranch,"SW"); + this._insertRegion(parentBranch,"SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch,containedNode); } - else { - this.constants.hierarchicalLayout.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = false; - this.constants.physics.barnesHut.enabled = true; + }; + + + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + exports._insertRegion = function(parentBranch, region) { + var minX,maxX,minY,maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; } - this._loadSelectedForceSolver(); - var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); - if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} - else {graph_toggleSmooth.style.background = "#FF8532";} - this.moving = true; - this.start(); - } + + + parentBranch.children[region] = { + centerOfMass:{x:0,y:0}, + mass:0, + range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: {data:null}, + maxWidth: 0, + level: parentBranch.level+1, + childrenCount: 0 + }; + }; /** - * this generates the ranges depending on the iniital values. + * This function is for debugging purposed, it draws the tree. * - * @param id - * @param map - * @param constantsVariableName + * @param ctx + * @param color + * @private */ - function showValueOfRange (id,map,constantsVariableName) { - var valueId = id + "_value"; - var rangeValue = document.getElementById(id).value; + exports._drawTree = function(ctx,color) { + if (this.barnesHutTree !== undefined) { - if (Array.isArray(map)) { - document.getElementById(valueId).value = map[parseInt(rangeValue)]; - this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root,ctx,color); } - else { - document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); - this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); + }; + + + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + exports._drawBranch = function(branch,ctx,color) { + if (color === undefined) { + color = "#FF0000"; } - if (constantsVariableName == "hierarchicalLayout_direction" || - constantsVariableName == "hierarchicalLayout_levelSeparation" || - constantsVariableName == "hierarchicalLayout_nodeSpacing") { - this._setupHierarchicalLayout(); + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW,ctx); + this._drawBranch(branch.children.NE,ctx); + this._drawBranch(branch.children.SE,ctx); + this._drawBranch(branch.children.SW,ctx); } - this.moving = true; - this.start(); - } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX,branch.range.minY); + ctx.lineTo(branch.range.maxX,branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX,branch.range.minY); + ctx.lineTo(branch.range.maxX,branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX,branch.range.maxY); + ctx.lineTo(branch.range.minX,branch.range.maxY); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(branch.range.minX,branch.range.maxY); + ctx.lineTo(branch.range.minX,branch.range.minY); + ctx.stroke(); + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + }; /***/ }, -/* 63 */ +/* 64 */ /***/ function(module, exports, __webpack_require__) { /** @@ -31912,11 +32361,11 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 64 */ +/* 65 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var Node = __webpack_require__(53); + var Node = __webpack_require__(56); /** * Creation of the SectorMixin var. @@ -32471,10 +32920,10 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 65 */ +/* 66 */ /***/ function(module, exports, __webpack_require__) { - var Node = __webpack_require__(53); + var Node = __webpack_require__(56); /** * This function can be called from the _doInAllSectors function @@ -32988,7 +33437,7 @@ return /******/ (function(modules) { // webpackBootstrap canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} } this.emit("click", properties); - this._redraw(); + this._requestRedraw(); }; @@ -33032,7 +33481,7 @@ return /******/ (function(modules) { // webpackBootstrap this._selectObject(edge,true); } } - this._redraw(); + this._requestRedraw(); }; @@ -33185,12 +33634,12 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 66 */ +/* 67 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var Node = __webpack_require__(53); - var Edge = __webpack_require__(52); + var Node = __webpack_require__(56); + var Edge = __webpack_require__(57); /** * clears the toolbar div element of children @@ -33711,432 +34160,28 @@ return /******/ (function(modules) { // webpackBootstrap exports._addNode = function() { if (this._selectionIsEmpty() && this.editMode == true) { var positionObject = this._pointerToPositionObject(this.pointerPosition); - var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; - if (this.triggerFunctions.add) { - if (this.triggerFunctions.add.length == 2) { - var me = this; - this.triggerFunctions.add(defaultData, function(finalizedData) { - me.nodesData.add(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for add does not support two arguments (data,callback)'); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - else { - this.nodesData.add(defaultData); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - }; - - - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._createEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.connect) { - if (this.triggerFunctions.connect.length == 2) { - var me = this; - this.triggerFunctions.connect(defaultData, function(finalizedData) { - me.edgesData.add(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for connect does not support two arguments (data,callback)'); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.add(defaultData); - this.moving = true; - this.start(); - } - } - }; - - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._editEdge = function(sourceNodeId,targetNodeId) { - if (this.editMode == true) { - var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; - if (this.triggerFunctions.editEdge) { - if (this.triggerFunctions.editEdge.length == 2) { - var me = this; - this.triggerFunctions.editEdge(defaultData, function(finalizedData) { - me.edgesData.update(finalizedData); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - this.moving = true; - this.start(); - } - } - else { - this.edgesData.update(defaultData); - this.moving = true; - this.start(); - } - } - }; - - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * - * @private - */ - exports._editNode = function() { - if (this.triggerFunctions.edit && this.editMode == true) { - var node = this._getSelectedNode(); - var data = {id:node.id, - label: node.label, - group: node.options.group, - shape: node.options.shape, - color: { - background:node.options.color.background, - border:node.options.color.border, - highlight: { - background:node.options.color.highlight.background, - border:node.options.color.highlight.border - } - }}; - if (this.triggerFunctions.edit.length == 2) { - var me = this; - this.triggerFunctions.edit(data, function (finalizedData) { - me.nodesData.update(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } - else { - throw new Error('No edit function has been bound to this button'); - } - }; - - - - - /** - * delete everything in the selection - * - * @private - */ - exports._deleteSelected = function() { - if (!this._selectionIsEmpty() && this.editMode == true) { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - if (this.triggerFunctions.del) { - var me = this; - var data = {nodes: selectedNodes, edges: selectedEdges}; - if (this.triggerFunctions.del.length == 2) { - this.triggerFunctions.del(data, function (finalizedData) { - me.edgesData.remove(finalizedData.edges); - me.nodesData.remove(finalizedData.nodes); - me._unselectAll(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for delete does not support two arguments (data, callback)') - } - } - else { - this.edgesData.remove(selectedEdges); - this.nodesData.remove(selectedNodes); - this._unselectAll(); - this.moving = true; - this.start(); - } - } - else { - alert(this.constants.locales[this.constants.locale]["deleteClusterError"]); - } - } - }; - - -/***/ }, -/* 67 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - - exports._cleanNavigation = function() { - // clean hammer bindings - if (this.navigationHammers.existing.length != 0) { - for (var i = 0; i < this.navigationHammers.existing.length; i++) { - this.navigationHammers.existing[i].dispose(); - } - this.navigationHammers.existing = []; - } - - this._navigationReleaseOverload = function () {}; - - // clean up previous navigation items - if (this.navigationDivs && this.navigationDivs['wrapper'] && this.navigationDivs['wrapper'].parentNode) { - this.navigationDivs['wrapper'].parentNode.removeChild(this.navigationDivs['wrapper']); - } - }; - - /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. - * - * @private - */ - exports._loadNavigationElements = function() { - this._cleanNavigation(); - - this.navigationDivs = {}; - var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; - var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent']; - - this.navigationDivs['wrapper'] = document.createElement('div'); - this.frame.appendChild(this.navigationDivs['wrapper']); - - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDivs[navigationDivs[i]] = document.createElement('div'); - this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i]; - this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); - - var hammer = Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true}); - hammer.on('touch', this[navigationDivActions[i]].bind(this)); - this.navigationHammers._new.push(hammer); - } - - this._navigationReleaseOverload = this._stopMovement; - - this.navigationHammers.existing = this.navigationHammers._new; - }; - - - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._zoomExtent = function(event) { - this.zoomExtent({duration:700}); - event.stopPropagation(); - }; - - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._stopMovement = function() { - this._xStopMoving(); - this._yStopMoving(); - this._stopZoom(); - }; - - - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - exports._moveUp = function(event) { - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * move the screen down - * @private - */ - exports._moveDown = function(event) { - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * move the screen left - * @private - */ - exports._moveLeft = function(event) { - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * move the screen right - * @private - */ - exports._moveRight = function(event) { - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * Zoom in, using the same method as the movement. - * @private - */ - exports._zoomIn = function(event) { - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * Zoom out - * @private - */ - exports._zoomOut = function(event) { - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; - - - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - exports._stopZoom = function(event) { - this.zoomIncrement = 0; - event && event.preventDefault(); - }; - - - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - exports._yStopMoving = function(event) { - this.yIncrement = 0; - event && event.preventDefault(); - }; - - - /** - * Stop moving in the X direction and unHighlight left and right. - * @private - */ - exports._xStopMoving = function(event) { - this.xIncrement = 0; - event && event.preventDefault(); - }; - - -/***/ }, -/* 68 */ -/***/ function(module, exports, __webpack_require__) { - - exports._resetLevels = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; - node.hierarchyEnumerated = false; - } - } - } - }; - - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - exports._setupHierarchicalLayout = function() { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; - - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; - } - else { - undefinedLevel = true; - } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; - } + var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function(finalizedData) { + me.nodesData.add(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); } - } - - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent({duration:0},true,this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { + else { + throw new Error('The function for add does not support two arguments (data,callback)'); + this._createManipulatorBar(); + this.moving = true; this.start(); } } else { - // setup the system to use hierarchical method. - this._changeConstants(); - - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.constants.hierarchicalLayout.layout == "hubsize") { - this._determineLevels(hubsize); - } - else { - this._determineLevelsDirected(false); - } - - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - - // place the nodes on the canvas. This also stablilizes the system. - this._placeNodesByHierarchy(distribution); - - // start the simulation. + this.nodesData.add(defaultData); + this._createManipulatorBar(); + this.moving = true; this.start(); } } @@ -34144,962 +34189,1004 @@ return /******/ (function(modules) { // webpackBootstrap /** - * This function places the nodes on the canvas based on the hierarchial distribution. + * connect two nodes with a new edge. * - * @param {Object} distribution | obtained by the function this._getDistribution() * @private */ - exports._placeNodesByHierarchy = function(distribution) { - var nodeId, node; - - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - node = distribution[level].nodes[nodeId]; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[level].minPos; - node.xFixed = false; - - distribution[level].minPos += distribution[level].nodeSpacing; - } - } - else { - if (node.yFixed) { - node.y = distribution[level].minPos; - node.yFixed = false; - - distribution[level].minPos += distribution[level].nodeSpacing; - } - } - this._placeBranchNodes(node.edges,node.id,distribution,node.level); - } + exports._createEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function(finalizedData) { + me.edgesData.add(finalizedData); + me.moving = true; + me.start(); + }); + } + else { + throw new Error('The function for connect does not support two arguments (data,callback)'); + this.moving = true; + this.start(); } } + else { + this.edgesData.add(defaultData); + this.moving = true; + this.start(); + } } - - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); }; - /** - * This function get the distribution of levels based on hubsize + * connect two nodes with a new edge. * - * @returns {Object} * @private */ - exports._getDistribution = function() { - var distribution = {}; - var nodeId, node, level; - - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; + exports._editEdge = function(sourceNodeId,targetNodeId) { + if (this.editMode == true) { + var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; + if (this.triggerFunctions.editEdge) { + if (this.triggerFunctions.editEdge.length == 2) { + var me = this; + this.triggerFunctions.editEdge(defaultData, function(finalizedData) { + me.edgesData.update(finalizedData); + me.moving = true; + me.start(); + }); } else { - node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; - } - if (distribution[node.level] === undefined) { - distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; - } - distribution[node.level].amount += 1; - distribution[node.level].nodes[nodeId] = node; - } - } - - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; + throw new Error('The function for edit does not support two arguments (data, callback)'); + this.moving = true; + this.start(); } } - } - - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= (distribution[level].amount + 1); - distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); + else { + this.edgesData.update(defaultData); + this.moving = true; + this.start(); } } - - return distribution; }; - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. * - * @param hubsize * @private */ - exports._determineLevels = function(hubsize) { - var nodeId, node; - - // determine hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; - } + exports._editNode = function() { + if (this.triggerFunctions.edit && this.editMode == true) { + var node = this._getSelectedNode(); + var data = {id:node.id, + label: node.label, + group: node.options.group, + shape: node.options.shape, + color: { + background:node.options.color.background, + border:node.options.color.border, + highlight: { + background:node.options.color.highlight.background, + border:node.options.color.highlight.border + } + }}; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.edit(data, function (finalizedData) { + me.nodesData.update(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); } - } - - // branch from hubs - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1,node.edges,node.id); - } + else { + throw new Error('The function for edit does not support two arguments (data, callback)'); } } + else { + throw new Error('No edit function has been bound to this button'); + } }; + /** - * this function allocates nodes in levels based on the direction of the edges + * delete everything in the selection * - * @param hubsize * @private */ - exports._determineLevelsDirected = function() { - var nodeId, node, firstNode; - var minLevel = 10000; - - // set first node to source - firstNode = this.nodes[this.nodeIndices[0]]; - firstNode.level = minLevel; - this._setLevelDirected(minLevel,firstNode.edges,firstNode.id); - - // get the minimum level - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - minLevel = node.level < minLevel ? node.level : minLevel; + exports._deleteSelected = function() { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.del) { + var me = this; + var data = {nodes: selectedNodes, edges: selectedEdges}; + if (this.triggerFunctions.del.length == 2) { + this.triggerFunctions.del(data, function (finalizedData) { + me.edgesData.remove(finalizedData.edges); + me.nodesData.remove(finalizedData.nodes); + me._unselectAll(); + me.moving = true; + me.start(); + }); + } + else { + throw new Error('The function for delete does not support two arguments (data, callback)') + } + } + else { + this.edgesData.remove(selectedEdges); + this.nodesData.remove(selectedNodes); + this._unselectAll(); + this.moving = true; + this.start(); + } } - } - - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - node.level -= minLevel; + else { + alert(this.constants.locales[this.constants.locale]["deleteClusterError"]); } } }; - /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * - * @private - */ - exports._changeConstants = function() { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.dynamic = false; - } - this._configureSmoothCurves(); +/***/ }, +/* 68 */ +/***/ function(module, exports, __webpack_require__) { - var config = this.constants.hierarchicalLayout; - config.levelSeparation = Math.abs(config.levelSeparation); - if (config.direction == "RL" || config.direction == "DU") { - config.levelSeparation *= -1; - } + var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); - if (config.direction == "RL" || config.direction == "LR") { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "vertical"; + exports._cleanNavigation = function() { + // clean hammer bindings + if (this.navigationHammers.existing.length != 0) { + for (var i = 0; i < this.navigationHammers.existing.length; i++) { + this.navigationHammers.existing[i].dispose(); } + this.navigationHammers.existing = []; } - else { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "horizontal"; - } + + this._navigationReleaseOverload = function () {}; + + // clean up previous navigation items + if (this.navigationDivs && this.navigationDivs['wrapper'] && this.navigationDivs['wrapper'].parentNode) { + this.navigationDivs['wrapper'].parentNode.removeChild(this.navigationDivs['wrapper']); } }; - /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel * @private */ - exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } + exports._loadNavigationElements = function() { + this._cleanNavigation(); - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } - else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } + this.navigationDivs = {}; + var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; + var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent']; - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); - } - } + this.navigationDivs['wrapper'] = document.createElement('div'); + this.frame.appendChild(this.navigationDivs['wrapper']); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDivs[navigationDivs[i]] = document.createElement('div'); + this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i]; + this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); + + var hammer = Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true}); + hammer.on('touch', this[navigationDivActions[i]].bind(this)); + this.navigationHammers._new.push(hammer); } + + this._navigationReleaseOverload = this._stopMovement; + + this.navigationHammers.existing = this.navigationHammers._new; }; /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * this stops all movement induced by the navigation buttons * - * @param level - * @param edges - * @param parentId * @private */ - exports._setLevel = function(level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (childNode.edges.length > 1) { - this._setLevel(level+1, childNode.edges, childNode.id); - } - } - } + exports._zoomExtent = function(event) { + this.zoomExtent({duration:700}); + event.stopPropagation(); }; - /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction + * this stops all movement induced by the navigation buttons * - * @param level - * @param edges - * @param parentId * @private */ - exports._setLevelDirected = function(level, edges, parentId) { - this.nodes[parentId].hierarchyEnumerated = true; - var childNode, direction; - for (var i = 0; i < edges.length; i++) { - direction = 1; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - direction = -1; - } - else { - childNode = edges[i].to; - } - if (childNode.level == -1) { - childNode.level = level + direction; - } - } - - for (var i = 0; i < edges.length; i++) { - if (edges[i].toId == parentId) {childNode = edges[i].from;} - else {childNode = edges[i].to;} - - if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { - this._setLevelDirected(childNode.level, childNode.edges, childNode.id); - } - } + exports._stopMovement = function() { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); }; /** - * Unfix nodes + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. * * @private */ - exports._restoreNodes = function() { - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - this.nodes[nodeId].xFixed = false; - this.nodes[nodeId].yFixed = false; - } - } + exports._moveUp = function(event) { + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; -/***/ }, -/* 69 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * + * move the screen down * @private */ - exports._calculateNodeForces = function () { - var dx, dy, angle, distance, fx, fy, combinedClusterSize, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // approximation constants - var a_base = -2 / 3; - var b = 4 / 3; - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.repulsion.nodeDistance; - var minimumDistance = nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // same condition as BarnesHut, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } + exports._moveDown = function(event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); - var a = a_base / minimumDistance; - if (distance < 2 * minimumDistance) { - if (distance < 0.5 * minimumDistance) { - repulsingForce = 1.0; - } - else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) - } - // amplify the repulsion for clusters. - repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; - repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); + /** + * move the screen left + * @private + */ + exports._moveLeft = function(event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - fx = dx * repulsingForce; - fy = dy * repulsingForce; - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; - } - } - } + /** + * move the screen right + * @private + */ + exports._moveRight = function(event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); }; -/***/ }, -/* 70 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Zoom in, using the same method as the movement. + * @private + */ + exports._zoomIn = function(event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; + /** - * Calculate the forces the nodes apply on eachother based on a repulsion field. - * This field is linearly approximated. - * + * Zoom out * @private */ - exports._calculateNodeForces = function () { - var dx, dy, distance, fx, fy, - repulsingForce, node1, node2, i, j; + exports._zoomOut = function(event) { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - // repulsing forces between nodes - var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + exports._stopZoom = function(event) { + this.zoomIncrement = 0; + event && event.preventDefault(); + }; - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - // nodes only affect nodes on their level - if (node1.level == node2.level) { + /** + * Stop moving in the Y direction and unHighlight the up and down + * @private + */ + exports._yStopMoving = function(event) { + this.yIncrement = 0; + event && event.preventDefault(); + }; - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + exports._xStopMoving = function(event) { + this.xIncrement = 0; + event && event.preventDefault(); + }; - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); - } - else { - repulsingForce = 0; - } - // normalize force with - if (distance == 0) { - distance = 0.01; - } - else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; +/***/ }, +/* 69 */ +/***/ function(module, exports, __webpack_require__) { + + exports._resetLevels = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + node.hierarchyEnumerated = false; } } } }; - /** - * this function calculates the effects of the springs in the case of unsmooth curves. + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly * * @private */ - exports._calculateHierarchicalSpringForces = function () { - var edgeLength, edge, edgeId; - var dx, dy, fx, fy, springForce, distance; - var edges = this.edges; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; + exports._setupHierarchicalLayout = function() { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } + else { + undefinedLevel = true; + } + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } - for (var i = 0; i < nodeIndices.length; i++) { - var node1 = nodes[nodeIndices[i]]; - node1.springFx = 0; - node1.springFy = 0; - } + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + this.zoomExtent({duration:0},true,this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } + else { + // setup the system to use hierarchical method. + this._changeConstants(); + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.constants.hierarchicalLayout.layout == "hubsize") { + this._determineLevels(hubsize); + } + else { + this._determineLevelsDirected(false); + } - // forces caused by the edges, modelled as springs - for (edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - if (edge.connected) { - // only calculate forces if nodes are in the same sector - if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { - edgeLength = edge.physics.springLength; - // this implies that the edges between big clusters are longer - edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - dx = (edge.from.x - edge.to.x); - dy = (edge.from.y - edge.to.y); - distance = Math.sqrt(dx * dx + dy * dy); + // place the nodes on the canvas. This also stablilizes the system. + this._placeNodesByHierarchy(distribution); - if (distance == 0) { - distance = 0.01; - } + // start the simulation. + this.start(); + } + } + }; - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; - fx = dx * springForce; - fy = dy * springForce; + /** + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private + */ + exports._placeNodesByHierarchy = function(distribution) { + var nodeId, node; + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + node = distribution[level].nodes[nodeId]; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[level].minPos; + node.xFixed = false; - if (edge.to.level != edge.from.level) { - edge.to.springFx -= fx; - edge.to.springFy -= fy; - edge.from.springFx += fx; - edge.from.springFy += fy; + distribution[level].minPos += distribution[level].nodeSpacing; + } } else { - var factor = 0.5; - edge.to.fx -= factor*fx; - edge.to.fy -= factor*fy; - edge.from.fx += factor*fx; - edge.from.fy += factor*fy; + if (node.yFixed) { + node.y = distribution[level].minPos; + node.yFixed = false; + + distribution[level].minPos += distribution[level].nodeSpacing; + } } + this._placeBranchNodes(node.edges,node.id,distribution,node.level); } } } } - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); - springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); - - node.fx += springFx; - node.fy += springFy; - } - - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - totalFx += node.fx; - totalFy += node.fy; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; - - for (i = 0; i < nodeIndices.length; i++) { - var node = nodes[nodeIndices[i]]; - node.fx -= correctionFx; - node.fy -= correctionFy; - } - + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); }; -/***/ }, -/* 71 */ -/***/ function(module, exports, __webpack_require__) { /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. + * This function get the distribution of levels based on hubsize * + * @returns {Object} * @private */ - exports._calculateNodeForces = function() { - if (this.constants.physics.barnesHut.gravitationalConstant != 0) { - var node; - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - var nodeCount = nodeIndices.length; - - this._formBarnesHutTree(nodes,nodeIndices); + exports._getDistribution = function() { + var distribution = {}; + var nodeId, node, level; - var barnesHutTree = this.barnesHutTree; + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + else { + node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; + } + if (distribution[node.level] === undefined) { + distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[nodeId] = node; + } + } - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHut condition - this._getForceContribution(barnesHutTree.root.children.NW,node); - this._getForceContribution(barnesHutTree.root.children.NE,node); - this._getForceContribution(barnesHutTree.root.children.SW,node); - this._getForceContribution(barnesHutTree.root.children.SE,node); + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; } } } + + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= (distribution[level].amount + 1); + distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); + } + } + + return distribution; }; /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. + * this function allocates nodes in levels based on the recursive branching from the largest hubs. * - * @param parentBranch - * @param node + * @param hubsize * @private */ - exports._getForceContribution = function(parentBranch,node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx,dy,distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); + exports._determineLevels = function(hubsize) { + var nodeId, node; - // BarnesHut condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; + // determine hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; } - else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW,node); - this._getForceContribution(parentBranch.children.NE,node); - this._getForceContribution(parentBranch.children.SW,node); - this._getForceContribution(parentBranch.children.SE,node); - } - else { // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5*Math.random(); - dx = distance; - } - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } + } + + // branch from hubs + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1,node.edges,node.id); } } } }; + + /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * this function allocates nodes in levels based on the direction of the edges * - * @param nodes - * @param nodeIndices + * @param hubsize * @private */ - exports._formBarnesHutTree = function(nodes,nodeIndices) { - var node; - var nodeCount = nodeIndices.length; + exports._determineLevelsDirected = function() { + var nodeId, node, firstNode; + var minLevel = 10000; - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX =-Number.MAX_VALUE, - maxY =-Number.MAX_VALUE; + // set first node to source + firstNode = this.nodes[this.nodeIndices[0]]; + firstNode.level = minLevel; + this._setLevelDirected(minLevel,firstNode.edges,firstNode.id); - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { minX = x; } - if (x > maxX) { maxX = x; } - if (y < minY) { minY = y; } - if (y > maxY) { maxY = y; } + // get the minimum level + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + minLevel = node.level < minLevel ? node.level : minLevel; } } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize - else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize - - - var minimumTreeSize = 1e-5; - var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root:{ - centerOfMass: {x:0, y:0}, - mass:0, - range: { - minX: centerX-halfRootSize,maxX:centerX+halfRootSize, - minY: centerY-halfRootSize,maxY:centerY+halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data:null}, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root,node); + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.level -= minLevel; } } - - // make global - this.barnesHutTree = barnesHutTree }; /** - * this updates the mass of a branch. this is increased by adding a node. + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. * - * @param parentBranch - * @param node * @private */ - exports._updateBranchMass = function(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1/totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; + exports._changeConstants = function() { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } + this._configureSmoothCurves(); - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); - parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; + var config = this.constants.hierarchicalLayout; + config.levelSeparation = Math.abs(config.levelSeparation); + if (config.direction == "RL" || config.direction == "DU") { + config.levelSeparation *= -1; + } + if (config.direction == "RL" || config.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; + } + } + else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } }; /** - * determine in which branch the node will be placed. + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. * - * @param parentBranch - * @param node - * @param skipMassUpdate + * @param edges + * @param parentId + * @param distribution + * @param parentLevel * @private */ - exports._placeInTree = function(parentBranch,node,skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch,node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { // in NW - this._placeInRegion(parentBranch,node,"NW"); + exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; } - else { // in SW - this._placeInRegion(parentBranch,node,"SW"); + else { + childNode = edges[i].to; } - } - else { // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { // in NE - this._placeInRegion(parentBranch,node,"NE"); + + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } } - else { // in SE - this._placeInRegion(parentBranch,node,"SE"); + else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } + + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); + } } } }; /** - * actually place the node in a region (or branch) + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. * - * @param parentBranch - * @param node - * @param region + * @param level + * @param edges + * @param parentId * @private */ - exports._placeInRegion = function(parentBranch,node,region) { - switch (parentBranch.children[region].childrenCount) { - case 0: // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region],node); - break; - case 1: // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && - parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } - else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region],node); + exports._setLevel = function(level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } + else { + childNode = edges[i].to; + } + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (childNode.edges.length > 1) { + this._setLevel(level+1, childNode.edges, childNode.id); } - break; - case 4: // place in branch - this._placeInTree(parentBranch.children[region],node); - break; + } } }; /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction * - * @param parentBranch + * @param level + * @param edges + * @param parentId * @private */ - exports._splitBranch = function(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; + exports._setLevelDirected = function(level, edges, parentId) { + this.nodes[parentId].hierarchyEnumerated = true; + var childNode, direction; + for (var i = 0; i < edges.length; i++) { + direction = 1; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + direction = -1; + } + else { + childNode = edges[i].to; + } + if (childNode.level == -1) { + childNode.level = level + direction; + } } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch,"NW"); - this._insertRegion(parentBranch,"NE"); - this._insertRegion(parentBranch,"SW"); - this._insertRegion(parentBranch,"SE"); - if (containedNode != null) { - this._placeInTree(parentBranch,containedNode); + for (var i = 0; i < edges.length; i++) { + if (edges[i].toId == parentId) {childNode = edges[i].from;} + else {childNode = edges[i].to;} + + if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { + this._setLevelDirected(childNode.level, childNode.edges, childNode.id); + } } }; /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch + * Unfix nodes * - * @param parentBranch - * @param region - * @param parentRange * @private */ - exports._insertRegion = function(parentBranch, region) { - var minX,maxX,minY,maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; + exports._restoreNodes = function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.nodes[nodeId].xFixed = false; + this.nodes[nodeId].yFixed = false; + } } + }; - parentBranch.children[region] = { - centerOfMass:{x:0,y:0}, - mass:0, - range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: {data:null}, - maxWidth: 0, - level: parentBranch.level+1, - childrenCount: 0 - }; +/***/ }, +/* 70 */ +/***/ function(module, exports, __webpack_require__) { + + // English + exports['en'] = { + edit: 'Edit', + del: 'Delete selected', + back: 'Back', + addNode: 'Add Node', + addEdge: 'Add Edge', + editNode: 'Edit Node', + editEdge: 'Edit Edge', + addDescription: 'Click in an empty space to place a new node.', + edgeDescription: 'Click on a node and drag the edge to another node to connect them.', + editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.', + createEdgeError: 'Cannot link edges to a cluster.', + deleteClusterError: 'Clusters cannot be deleted.' + }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; + + // Dutch + exports['nl'] = { + edit: 'Wijzigen', + del: 'Selectie verwijderen', + back: 'Terug', + addNode: 'Node toevoegen', + addEdge: 'Link toevoegen', + editNode: 'Node wijzigen', + editEdge: 'Link wijzigen', + addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.', + edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.', + editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.', + createEdgeError: 'Kan geen link maken naar een cluster.', + deleteClusterError: 'Clusters kunnen niet worden verwijderd.' }; + exports['nl_NL'] = exports['nl']; + exports['nl_BE'] = exports['nl']; + +/***/ }, +/* 71 */ +/***/ function(module, exports, __webpack_require__) { /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color - * @private + * Canvas shapes used by Network */ - exports._drawTree = function(ctx,color) { - if (this.barnesHutTree !== undefined) { + if (typeof CanvasRenderingContext2D !== 'undefined') { - ctx.lineWidth = 1; + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; - this._drawBranch(this.barnesHutTree.root,ctx,color); - } - }; + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); - /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ - exports._drawBranch = function(branch,ctx,color) { - if (color === undefined) { - color = "#FF0000"; - } + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW,ctx); - this._drawBranch(branch.children.NE,ctx); - this._drawBranch(branch.children.SE,ctx); - this._drawBranch(branch.children.SW,ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.minY); - ctx.stroke(); + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.maxY); - ctx.stroke(); + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.maxY); - ctx.stroke(); + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.minY); - ctx.stroke(); + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle */ - }; + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep; + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + }; + + // TODO: add diamond shape + } /***/ } diff --git a/examples/network/06_groups.html b/examples/network/06_groups.html index 19964045..076cd5c6 100644 --- a/examples/network/06_groups.html +++ b/examples/network/06_groups.html @@ -141,9 +141,11 @@ var options = { stabilize: false, nodes: { - shape: 'dot' + shape: 'dot', + radius:30, + borderWidth:2 }, - physics: {barnesHut:{springLength: 200}} + physics: {barnesHut:{springLength: 100}} }; network = new vis.Network(container, data, options); } @@ -154,9 +156,9 @@
Number of groups: - + Number of nodes per group: - +

diff --git a/examples/network/26_staticSmoothCurves.html b/examples/network/26_staticSmoothCurves.html index 1c338269..d6b06dde 100644 --- a/examples/network/26_staticSmoothCurves.html +++ b/examples/network/26_staticSmoothCurves.html @@ -32,19 +32,26 @@ Smooth curve type: + + +
+Roundness (0..1): (0.5 is max roundness for continuous, 1.0 for the others)
diff --git a/examples/network/27_world_cup_network.html b/examples/network/27_world_cup_network.html index 54e85cb5..7361849d 100644 --- a/examples/network/27_world_cup_network.html +++ b/examples/network/27_world_cup_network.html @@ -39,6 +39,8 @@ Smooth curve type: + +
inheritColor option: