From 2c6928d2561daee49b5c314c44a17e563d7ddcd4 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Mon, 17 Feb 2014 21:23:12 +0100 Subject: [PATCH] Tweaked physics system, added documentation for all new features (physics, manipulation, smooth) made all GUI elements CSS and HTML. --- Jakefile.js | 8 +- docs/graph.html | 327 ++++++++++-- examples/graph/02_random_nodes.html | 2 - examples/graph/20_navigation.html | 3 +- examples/graph/21_data_manipulation.html | 482 +++++++----------- src/graph/Edge.js | 8 +- src/graph/Graph.js | 331 +++++++----- src/graph/Node.js | 48 +- src/graph/css/graph-manipulation.css | 128 +++++ src/graph/css/graph-navigation.css | 62 +++ src/graph/graphMixins/ClusterMixin.js | 9 +- src/graph/graphMixins/ManipulationMixin.js | 83 ++- src/graph/graphMixins/MixinLoader.js | 61 ++- src/graph/graphMixins/NavigationMixin.js | 136 ++--- src/graph/graphMixins/SectorsMixin.js | 42 +- src/graph/graphMixins/SelectionMixin.js | 63 +-- src/graph/graphMixins/physics/PhysicsMixin.js | 50 +- src/graph/graphMixins/physics/barnesHut.js | 76 +-- src/graph/graphMixins/physics/repulsion.js | 16 +- src/graph/img/cross.png | Bin 0 -> 18303 bytes src/graph/img/cross2.png | Bin 0 -> 17768 bytes 21 files changed, 1121 insertions(+), 814 deletions(-) create mode 100644 src/graph/css/graph-manipulation.css create mode 100644 src/graph/css/graph-navigation.css create mode 100644 src/graph/img/cross.png create mode 100644 src/graph/img/cross2.png diff --git a/Jakefile.js b/Jakefile.js index 971f2a7e..a36a934c 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -83,8 +83,8 @@ task('build', {async: true}, function () { './src/graph/Groups.js', './src/graph/Images.js', './src/graph/graphMixins/physics/PhysicsMixin.js', - './src/graph/graphMixins/physics/barnesHut.js', - './src/graph/graphMixins/physics/repulsion.js', + './src/graph/graphMixins/physics/BarnesHut.js', + './src/graph/graphMixins/physics/Repulsion.js', './src/graph/graphMixins/ManipulationMixin.js', './src/graph/graphMixins/SectorsMixin.js', './src/graph/graphMixins/ClusterMixin.js', @@ -103,6 +103,10 @@ task('build', {async: true}, function () { wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', { forceDelete: true }); + // copy css + wrench.copyDirSyncRecursive('./src/graph/css', DIST+ '/css', { + forceDelete: true + }); var timeStart = Date.now(); // bundle the concatenated script and dependencies into one file diff --git a/docs/graph.html b/docs/graph.html index 8dfe2594..37636cf4 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -53,7 +53,9 @@
  • Nodes
  • Edges
  • Groups
  • -
  • Clustering
  • +
  • Physics
  • +
  • Data_manipulation
  • +
  • Clustering
  • Navigation controls
  • Keyboard navigation
  • @@ -529,13 +531,6 @@ var edges = [ type. - - length - number - no - The length of the edge in pixels. - - style string @@ -647,6 +642,25 @@ var options = { Description + + physics + Object + none + + Configuration of the physics system governing the simulation of the nodes and edges. + Barnes-Hut nBody simulation is used by default. See section Physics for an overview of the available options. + + + + + dataManipulation + Object + none + + Settings for manipulating the Dataset. See section Data manipulation for an overview of the available options. + + + clustering Object @@ -710,6 +724,13 @@ var options = { + + smoothCurves + Boolean + true + If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve with control points on both nodes and an invisible node in the center of the edge. This support node is also handed by the physics simulation. + + selectable Boolean @@ -964,12 +985,6 @@ var options = { Only applicable when the line style is dash-line. - - length - Number - 100 - The default length of a edge. - style String @@ -1122,6 +1137,235 @@ var nodes = [ +

    Physics

    +

    + The physics system has been overhauled to increase performance. The original simulation method was based on particel physics with a repulsion field (potential) around each node, + and the edges were modelled as springs. The new system employed the Barnes-Hut gravitational simulation model. The edges are still modelled as springs. + To unify the physics system, the damping, repulsion distance and edge length have been combined in an physics option. To retain good behaviour, both the old repulsion model and the Barnes-Hut model have their own parameters. + If no options for the physics system are supplied, the Barnes-Hut method will be used with the default parameters. +

    +
    +// These variables must be defined in an options object named physics.
    +// If a variable is not supplied, the default value is used.
    +var options = {
    +    physics: {
    +        barnesHut: {
    +            enabled: true,
    +            gravitationalConstant: -2000,
    +            centralGravity: 0.1,
    +            springLength: 100,
    +            springConstant: 0.05,
    +            damping: 0.09
    +        },
    +        repulsion: {
    +            centralGravity: 0.1,
    +            springLength: 50,
    +            springConstant: 0.05,
    +            nodeDistance: 100,
    +            damping: 0.09
    +        },
    +    }
    +
    +
    barnesHut:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    enabledBooleantrueThis switches the Barnes-Hut simulation on or off. If it is turned off, the old repulsion model is used. Barnes-Hut is generally faster and yields better results.
    gravitationalConstantNumber-2000This is the gravitational constand used to calculate the gravity forces. More information is available here.
    centralGravityNumber0.1The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
    springLengthNumber100In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields. + To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.
    springConstantNumber0.05This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.
    dampingNumber0.09This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available here.
    +
    repulsion:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    centralGravityNumber0.1The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
    springLengthNumber50In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields. + To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.
    nodeDistanceNumber100This parameter is used to define the distance of influence of the repulsion field of the nodes. Below half this distance, the repulsion is maximal and beyond twice this distance the repulsion is zero.
    springConstantNumber0.05This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.
    dampingNumber0.09This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available here.
    + +

    Data manipulation

    +

    + By using the data manipulation feature of the graph you can dynamically create nodes, connect nodes with edges, edit nodes or delete nodes and edges. + The toolbar is fully HTML and CSS so the user can style this to their preference. To control the behaviour of the data manipulation, users can insert custom functions + into the data manipulation process. For example, an injected function can show an detailed pop-up when a user wants to add a node. In example 21, + two functions have been injected into the add and edit functionality. This is described in more detail in the next subsection. +

    +
    +// These variables must be defined in an options object named dataManipulation.
    +// If a variable is not supplied, the default value is used.
    +var options = {
    +    dataManipulation: {
    +      enabled: false,
    +      initiallyVisible: false
    +    }
    +}
    +// OR to just load the module with default values:
    +var options: {
    +    dataManipulation: true
    +}
    +
    + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    enabledBooleanfalseEnabling or disabling of the data manipulation toolbar. If it is initially hidden, an edit button appears in the top left corner.
    initiallyVisibleBooleanfalseInitially hide or show the data manipulation toolbar.
    + +

    Data manipulation: custom functionality

    +

    + Users can insert custom functions into the add node, edit node, connect nodes, and delete selected operations. This is done by supplying them in the options. + If the callback is NOT called, nothing happens. Example 21 has two working examples + for the add and edit functions. The data the user is supplied with in these functions has been described in the code below. + For the add data, you can add any and all options that are accepted for node creation as described above. The same goes for edit, however only the fields described + in the code below contain information on the selected node. The callback for connect accepts any options that are used for edge creation. Only the callback for delete selected + requires the same data structure that is supplied to the user. +

    +
    +// If a variable is not supplied, the default value is used.
    +var options: {
    +    dataManipulation: true,
    +    onAdd: function(data,callback) {
    +        // fixed must be false because we define a set x and y position.
    +        // If fixed is not false, the node cannot move.
    +        /** data = {id: random unique id,
    +        *           label: new,
    +        *           x: x position of click (canvas space),
    +        *           y: y position of click (canvas space),
    +        *           fixed: false
    +        *          };
    +        */
    +        var newData = {..}; // alter the data as you want.
    +                            // all fields normally accepted by a node can be used.
    +        callback(newData);  // call the callback to add a node.
    +    },
    +    onEdit: function(data,callback) {
    +        /** data = {id:...,
    +        *           label: ...,
    +        *           group: ...,
    +        *           shape: ...,
    +        *           color: {
    +        *             background:...,
    +        *             border:...,
    +        *             highlight: {
    +        *               background:...,
    +        *               border:...
    +        *             }
    +        *           }
    +        *          };
    +        */
    +        var newData = {..}; // alter the data as you want.
    +                            // all fields normally accepted by a node can be used.
    +        callback(newData);  // call the callback with the new data to edit the node.
    +    }
    +    onConnect: function(data,callback) {
    +        // data = {from: nodeId1, to: nodeId2};
    +        var newData = {..};      // check or alter data as you see fit.
    +        callback(newData);       // call the callback to connect the nodes.
    +    },
    +    onDelete: function(data,callback) {
    +        // data = {nodes: [selectedNodeIds], edges: [selectedEdgeIds]};
    +        var newData = {..}; // alter the data as you want.
    +                            // the same data structure is required.
    +        callback(newData);  // call the callback to delete the objects.
    +    }
    +};
    +
    +

    + Because the interface elements are CSS and HTML, the user will have to correct for size changes of the canvas. To facilitate this, a new event has been added called frameResize. + A function can be bound to this event. This function is supplied with the new widht and height of the canvas. The CSS can then be updated accordingly. + An code snippet from example 21 is shown below. +

    +
    +graph.on("frameResize", function(params) {console.log(params.width,params.height)});
    +
    +

    Clustering

    The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without @@ -1150,16 +1394,19 @@ var options = { reduceToNodes:300, chainThreshold: 0.4, clusterEdgeThreshold: 20, - sectorThreshold: 50, + sectorThreshold: 100, screenSizeThreshold: 0.2, fontSizeMultiplier: 4.0, - forceAmplification: 0.6, - distanceAmplification: 0.2, - edgeGrowth: 11, - nodeScaling: {width: 10, - height: 10, - radius: 10}, - activeAreaBoxSize: 100 + maxFontSize: 1000, + forceAmplification: 0.1, + distanceAmplification: 0.1, + edgeGrowth: 20, + nodeScaling: {width: 1, + height: 1, + radius: 1}, + maxNodeSizeIncrements: 600, + activeAreaBoxSize: 100, + clusterLevelDifference: 2 } } // OR to just load the module with default values: @@ -1233,6 +1480,12 @@ var options: { 4.0 This parameter denotes the increase in fontSize of the cluster when a single node is added to it. + + maxFontSize + Number + 1000 + This parameter denotes the largest allowed font size. If the font becomes too large, some browsers experience problems displaying this. + forceAmplification Number @@ -1251,7 +1504,7 @@ var options: { edgeGrowth Number - 11 + 20 This factor determines the elongation of edges connected to a cluster. @@ -1272,13 +1525,29 @@ var options: { 10 This factor determines how much the radius of a cluster increases in pixels per added node. - - activeAreaBoxSize + + maxNodeSizeIncrements + Number + 600 + This limits the size clusters can grow to. The default value, 600, implies that if a cluster contains more than 600 nodes, it will no longer grow. + + + activeAreaBoxSize + Number + 100 + Imagine a square with an edge length of activeAreaBoxSize pixels around your cursor. + If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector. + This is regardless of the zoom level. + + + clusterLevelDifference Number - 100 - Imagine a square with an edge length of activeAreaBoxSize pixels around your cursor. - If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector. - This is regardless of the zoom level. + 2 + At every clustering session, Graph will check if the difference between cluster levels is + acceptable. When a cluster is formed when zooming out, that is one cluster level. + If you zoom out further and it encompasses more nodes, that is another level. For example: + If the highest level of your graph at any given time is 3, nodes that have not clustered or + have clustered only once will join their neighbour with the lowest cluster level. diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html index ee648bbc..ea1202aa 100755 --- a/examples/graph/02_random_nodes.html +++ b/examples/graph/02_random_nodes.html @@ -102,8 +102,6 @@ -calculation time: ms -render time: ms

    diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html index 5bfaff77..8bc2006a 100644 --- a/examples/graph/20_navigation.html +++ b/examples/graph/20_navigation.html @@ -34,6 +34,7 @@ + - - + #operation { + font-size:28px; + } + #graph-popUp { + display:none; + position:absolute; + top:350px; + left:170px; + z-index:299; + width:250px; + height:120px; + background-color: #f9f9f9; + border-style:solid; + border-width:3px; + border-color: #5394ed; + padding:10px; + text-align: center; + } + + + + + + -

    Navigation controls and keyboad navigation

    +

    Editing the dataset

    - This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Icons:
    Keyboard shortcuts:
    Up arrow
    Down arrow
    Left arrow
    Right arrow
    =
    [
    Page up
    -
    ]
    Page down
    None
    Description:
    Move up
    Move down
    Move left
    Move right
    Zoom in
    Zoom out
    Zoom extends
    -
    - Apart from clicking the icons, you can also navigate using the keyboard. The buttons are in table above. - Zoom Extends changes the zoom and position of the camera to encompass all visible nodes. - - + In this example we have enabled the data manipulation setting. If the dataManipulation option is set to true, the edit button will appear. + If you prefer to have the toolbar visible initially, you can set the initiallyVisible option to true. The exact method is described in the docs. +

    + The data manipulation allows the user to add nodes, connect them, edit them and delete any selected items. In this example we have created trigger functions + for the add and edit operations. By settings these trigger functions the user can direct the way the data is manipulated. In this example we have created a simple + pop-up that allows us to edit some of the properties.

    - - - - -
    -
    - +
    + node
    + + + + + +
    id
    label
    + + +
    +

    + diff --git a/src/graph/Edge.js b/src/graph/Edge.js index ba5aa6da..eafcdfdb 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -231,7 +231,7 @@ Edge.prototype._drawLine = function(ctx) { ctx.lineWidth = this._getLineWidth(); var point; - if (this.from != this.to) { + if (this.from != this.to+9) { // draw line this._line(ctx); @@ -706,15 +706,15 @@ Edge.prototype.setScale = function(scale) { Edge.prototype.select = function() { this.selected = true; -} +}; Edge.prototype.unselect = function() { this.selected = false; -} +}; Edge.prototype.positionBezierNode = function() { if (this.via !== null) { this.via.x = 0.5 * (this.from.x + this.to.x); this.via.y = 0.5 * (this.from.y + this.to.y); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/graph/Graph.js b/src/graph/Graph.js index bbfff971..6c0ed49b 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -17,12 +17,17 @@ function Graph (container, data, options) { this.containerElement = container; this.width = '100%'; this.height = '100%'; - // to give everything a nice fluidity, we seperate the rendering and calculating of the forces - this.renderRefreshRate = 60; // hz (fps) + + // render and calculation settings + this.renderRefreshRate = 60; // hz (fps) this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.stabilize = true; // stabilize before displaying the graph + this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame + this.maxRenderSteps = 4; // max amount of physics ticks per render step. + + this.stabilize = true; // stabilize before displaying the graph this.selectable = true; - // these functions can be triggered when the dataset is edited + + // these functions are triggered when the dataset is edited this.triggerFunctions = {add:null,edit:null,connect:null,delete:null}; // set constant values @@ -61,8 +66,6 @@ function Graph (container, data, options) { fontColor: '#343434', fontSize: 14, // px fontFace: 'arial', - //distance: 100, //px - length: 100, // px dash: { length: 10, gap: 5, @@ -72,18 +75,21 @@ function Graph (container, data, options) { physics: { barnesHut: { enabled: true, - theta: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -3000, - centralGravity: 0.9, - springLength: 40, - springConstant: 0.04 + theta: 1 / 0.6, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.1, + springLength: 100, + springConstant: 0.05, + damping: 0.09 }, repulsion: { - centralGravity: 0.01, - springLength: 80, + centralGravity: 0.1, + springLength: 50, springConstant: 0.05, - nodeDistance: 100 + nodeDistance: 100, + damping: 0.09 }, + damping: null, centralGravity: null, springLength: null, springConstant: null @@ -98,8 +104,8 @@ function Graph (container, data, options) { sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). maxFontSize: 1000, + forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster. @@ -117,104 +123,101 @@ function Graph (container, data, options) { enabled: false, speed: {x: 10, y: 10, zoom: 0.02} }, - dataManipulationToolbar: { + dataManipulation: { enabled: false, initiallyVisible: false }, smoothCurves: true, - maxVelocity: 25, - minVelocity: 0.1, // px/s + maxVelocity: 10, + minVelocity: 0.1, // px/s maxIterations: 1000 // maximum number of iteration to stabilize }; - this.editMode = this.constants.dataManipulationToolbar.initiallyVisible; + this.editMode = this.constants.dataManipulation.initiallyVisible; // Node variables + var graph = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function () { graph._redraw(); }); - // navigation variables + + // keyboard navigation variables this.xIncrement = 0; this.yIncrement = 0; this.zoomIncrement = 0; + // loading all the mixins: // load the force calculation functions, grouped under the physics system. this._loadPhysicsSystem(); - // create a frame and canvas this._create(); - // load the sector system. (mandatory, fully integrated with Graph) this._loadSectorSystem(); - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) this._loadClusterSystem(); - // load the selection system. (mandatory, required by Graph) this._loadSelectionSystem(); - // apply options this.setOptions(options); // other vars - var graph = this; this.freezeSimulation = false;// freeze the simulation this.cachedFunctions = {}; + // containers for nodes and edges this.calculationNodes = {}; this.calculationNodeIndices = []; this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation this.nodes = {}; // object with Node objects this.edges = {}; // object with Edge objects + // position and scale variables and objects this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action this.scale = 1; // defining the global scale variable in the constructor this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out + // datasets or dataviews this.nodesData = null; // A DataSet or DataView this.edgesData = null; // A DataSet or DataView // create event listeners used to subscribe on the DataSets of the nodes and edges - var me = this; this.nodesListeners = { 'add': function (event, params) { - me._addNodes(params.items); - me.start(); + graph._addNodes(params.items); + graph.start(); }, 'update': function (event, params) { - me._updateNodes(params.items); - me.start(); + graph._updateNodes(params.items); + graph.start(); }, 'remove': function (event, params) { - me._removeNodes(params.items); - me.start(); + graph._removeNodes(params.items); + graph.start(); } }; this.edgesListeners = { 'add': function (event, params) { - me._addEdges(params.items); - me.start(); + graph._addEdges(params.items); + graph.start(); }, 'update': function (event, params) { - me._updateEdges(params.items); - me.start(); + graph._updateEdges(params.items); + graph.start(); }, 'remove': function (event, params) { - me._removeEdges(params.items); - me.start(); + graph._removeEdges(params.items); + graph.start(); } }; - // properties of the data + // properties for the animation this.moving = false; // True if any of the nodes have an undefined position - this.timer = undefined; - + this.timer = undefined; // Scheduling function. Is definded in this.start(); // load data (the disable start variable will be the same as the enabled clustering) this.setData(data,this.constants.clustering.enabled); @@ -314,10 +317,10 @@ Graph.prototype.zoomToFit = function(initialZoom, doNotStart) { if (initialZoom == true) { if (this.constants.clustering.enabled == true && numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. } else { - zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. } } else { @@ -418,11 +421,22 @@ Graph.prototype.setOptions = function (options) { if (options.height !== undefined) {this.height = options.height;} if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.selectable !== undefined) {this.selectable = options.selectable;} + if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;} - if (options.triggerFunctions) { - for (prop in options.triggerFunctions) { - this.triggerFunctions[prop] = options.triggerFunctions[prop]; + if (options.onAdd) { + this.triggerFunctions.add = options.onAdd; } + + if (options.onEdit) { + this.triggerFunctions.edit = options.onEdit; + } + + if (options.onConnect) { + this.triggerFunctions.connect = options.onConnect; + } + + if (options.onDelete) { + this.triggerFunctions.delete = options.onDelete; } if (options.physics) { @@ -481,16 +495,16 @@ Graph.prototype.setOptions = function (options) { this.constants.keyboard.enabled = false; } - if (options.dataManipulationToolbar) { - this.constants.dataManipulationToolbar.enabled = true; - for (prop in options.dataManipulationToolbar) { - if (options.dataManipulationToolbar.hasOwnProperty(prop)) { - this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop]; + if (options.dataManipulation) { + this.constants.dataManipulation.enabled = true; + for (prop in options.dataManipulation) { + if (options.dataManipulation.hasOwnProperty(prop)) { + this.constants.dataManipulation[prop] = options.dataManipulation[prop]; } } } - else if (options.dataManipulationToolbar !== undefined) { - this.constants.dataManipulationToolbar.enabled = false; + else if (options.dataManipulation !== undefined) { + this.constants.dataManipulation.enabled = false; } // TODO: work out these options and document them @@ -547,14 +561,17 @@ Graph.prototype.setOptions = function (options) { } } + + // (Re)loading the mixins that can be enabled or disabled in the options. // load the force calculation functions, grouped under the physics system. this._loadPhysicsSystem(); - // load the navigation system. this._loadNavigationControls(); - // load the data manipulation system this._loadManipulationSystem(); + // configure the smooth curves + this._configureSmoothCurves(); + // bind keys. If disabled, this will not do anything; this._createKeyBinds(); @@ -562,7 +579,7 @@ Graph.prototype.setOptions = function (options) { this.setSize(this.width, this.height); this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); this._setScale(1); - this.zoomToFit() + this.zoomToFit(); this._redraw(); }; @@ -575,7 +592,7 @@ Graph.prototype.setOptions = function (options) { * event specific properties. */ Graph.prototype.on = function on (event, callback) { - var available = ['select']; + var available = ['select','frameResize']; if (available.indexOf(event) == -1) { throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); @@ -693,14 +710,13 @@ Graph.prototype._createKeyBinds = function() { this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); } - this.mousetrap.bind("b",this._toggleBarnesHut.bind(me)); +// this.mousetrap.bind("b",this._toggleBarnesHut.bind(me)); - if (this.constants.dataManipulationToolbar.enabled == true) { + if (this.constants.dataManipulation.enabled == true) { this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); this.mousetrap.bind("del",this._deleteSelected.bind(me)); - this.mousetrap.bind("e",this._toggleEditMode.bind(me)); } -} +}; /** * Get the pointer location from a touch location @@ -737,6 +753,12 @@ Graph.prototype._onDragStart = function () { }; +/** + * This function is called by _onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ Graph.prototype._handleDragStart = function() { var drag = this.drag; var node = this._getNodeAt(drag.pointer); @@ -778,7 +800,7 @@ Graph.prototype._handleDragStart = function() { } } } -} +}; /** @@ -789,6 +811,13 @@ Graph.prototype._onDrag = function (event) { this._handleOnDrag(event) }; + +/** + * This function is called by _onDrag. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ Graph.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; @@ -817,7 +846,7 @@ Graph.prototype._handleOnDrag = function(event) { } }); - // start animation if not yet running + // start _animationStep if not yet running if (!this.moving) { this.moving = true; this.start(); @@ -834,7 +863,7 @@ Graph.prototype._handleOnDrag = function(event) { this._redraw(); this.moved = true; } -} +}; /** * handle drag start event @@ -938,8 +967,6 @@ Graph.prototype._zoom = function(scale, pointer) { this.areaCenter = {"x" : this._canvasToX(pointer.x), "y" : this._canvasToY(pointer.y)}; - // this.areaCenter = {"x" : pointer.x,"y" : pointer.y }; -// console.log(translation.x,translation.y,pointer.x,pointer.y,scale); this.pinch.mousewheelScale = scale; this._setScale(scale); this._setTranslation(tx, ty); @@ -949,6 +976,7 @@ Graph.prototype._zoom = function(scale, pointer) { return scale; }; + /** * Event handler for mouse wheel event, used to zoom the timeline * See http://adomas.org/javascript-mouse-wheel/ @@ -1098,6 +1126,7 @@ Graph.prototype._checkShowPopup = function (pointer) { } }; + /** * Check if the popup must be hided, which is the case when the mouse is no * longer hovering on the object @@ -1135,9 +1164,7 @@ Graph.prototype.setSize = function(width, height) { this.manipulationDiv.style.width = this.frame.canvas.clientWidth; } - if (this.constants.navigation.enabled == true) { - this._relocateNavigation(); - } + this._trigger('frameResize', {width:this.frame.canvas.width,height:this.frame.canvas.height}); }; /** @@ -1200,7 +1227,7 @@ Graph.prototype._addNodes = function(ids) { this.nodes[id] = node; // note: this may replace an existing node if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) { - var radius = this.constants.physics.springLength * 0.2*ids.length; + var radius = 10 * 0.1*ids.length; var angle = 2 * Math.PI * Math.random(); if (node.xFixed == false) {node.x = radius * Math.cos(angle);} if (node.yFixed == false) {node.y = radius * Math.sin(angle);} @@ -1211,7 +1238,7 @@ Graph.prototype._addNodes = function(ids) { } } this._updateNodeIndexList(); - this._setCalculationNodes() + this._updateCalculationNodes(); this._reconnectEdges(); this._updateValueRange(this.nodes); this.updateLabels(); @@ -1337,7 +1364,7 @@ Graph.prototype._addEdges = function (ids) { this.moving = true; this._updateValueRange(edges); this._createBezierNodes(); - this._setCalculationNodes(); + this._updateCalculationNodes(); }; /** @@ -1392,7 +1419,7 @@ Graph.prototype._removeEdges = function (ids) { this.moving = true; this._updateValueRange(edges); - this._setCalculationNodes(); + this._updateCalculationNodes(); }; /** @@ -1497,10 +1524,6 @@ Graph.prototype._redraw = function() { // restore original scaling and translation ctx.restore(); - - if (this.constants.navigation.enabled == true) { - this._doInNavigationSector("_drawNodes",ctx,true); - } }; /** @@ -1698,7 +1721,7 @@ Graph.prototype._isMoving = function(vmin) { * @private */ Graph.prototype._discreteStepNodes = function() { - var interval = 1.0; + var interval = 0.75; var nodes = this.nodes; var nodeId; @@ -1726,13 +1749,7 @@ Graph.prototype._discreteStepNodes = function() { }; - -/** - * Start animating nodes and edges - * - * @poram {Boolean} runCalculationStep - */ -Graph.prototype.start = function() { +Graph.prototype._physicsTick = function() { if (!this.freezeSimulation) { if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); @@ -1743,33 +1760,53 @@ Graph.prototype.start = function() { this._findCenter(this._getRange()) } } +}; - if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { - // start animation. only start calculationTimer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - - // keyboad movement - if (graph.xIncrement != 0 || graph.yIncrement != 0) { - var translation = graph._getTranslation(); - graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); - } - if (graph.zoomIncrement != 0) { - var center = { - x: graph.frame.canvas.clientWidth / 2, - y: graph.frame.canvas.clientHeight / 2 - }; - graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); - } +/** + * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick. + * It reschedules itself at the beginning of the function + * + * @private + */ +Graph.prototype._animationStep = function() { + // reset the timer so a new scheduled animation step can be set + this.timer = undefined; + + // handle the keyboad movement + this._handleNavigation(); + + // this schedules a new animation step + this.start(); - graph.start(); - graph.start(); - graph._redraw(); + // start the physics simulation + var calculationTime = Date.now(); + var maxSteps = 1; + this._physicsTick(); + var timeRequired = Date.now() - calculationTime; + while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxRenderSteps) { + this._physicsTick(); + timeRequired = Date.now() - calculationTime; + maxSteps++; + + } + + // start the rendering process + var renderTime = Date.now(); + this._redraw(); + this.renderTime = Date.now() - renderTime; +}; - }, this.renderTimestep); + +/** + * Schedule a animation step with the refreshrate interval. + * + * @poram {Boolean} runCalculationStep + */ +Graph.prototype.start = function() { + if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { + if (!this.timer) { + this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function } } else { @@ -1777,24 +1814,29 @@ Graph.prototype.start = function() { } }; + /** - * Debug function, does one step of the graph + * Move the graph according to the keyboard presses. + * + * @private */ -Graph.prototype.singleStep = function() { - if (this.moving) { - this._initializeForceCalculation(); - this._discreteStepNodes(); - - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - this._redraw(); +Graph.prototype._handleNavigation = function() { + if (this.xIncrement != 0 || this.yIncrement != 0) { + var translation = this._getTranslation(); + this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement); + } + if (this.zoomIncrement != 0) { + var center = { + x: this.frame.canvas.clientWidth / 2, + y: this.frame.canvas.clientHeight / 2 + }; + this._zoom(this.scale*(1 + this.zoomIncrement), center); } }; - /** - * Freeze the animation + * Freeze the _animationStep */ Graph.prototype.toggleFreeze = function() { if (this.freezeSimulation == false) { @@ -1807,24 +1849,43 @@ Graph.prototype.toggleFreeze = function() { }; + +Graph.prototype._configureSmoothCurves = function() { + if (this.constants.smoothCurves == true) { + this._createBezierNodes(); + } + else { + // delete the support nodes + this.sectors['support']['nodes'] = {}; + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.edges[edgeId].smooth = false; + this.edges[edgeId].via = null; + } + } + } + this._updateCalculationNodes(); + this.moving = true; + this.start(); +}; + Graph.prototype._createBezierNodes = function() { if (this.constants.smoothCurves == true) { for (var edgeId in this.edges) { if (this.edges.hasOwnProperty(edgeId)) { var edge = this.edges[edgeId]; - if (edge.smooth == true) { - if (edge.via == null) { - var nodeId = "edgeId:".concat(edge.id); - this.sectors['support']['nodes'][nodeId] = new Node( - {id:nodeId, - mass:1, - shape:'circle', - internalMultiplier:1, - damping: 1.2},{},{},this.constants); - edge.via = this.sectors['support']['nodes'][nodeId]; - edge.via.parentEdgeId = edge.id; - edge.positionBezierNode(); - } + if (edge.via == null) { + edge.smooth = true; + var nodeId = "edgeId:".concat(edge.id); + this.sectors['support']['nodes'][nodeId] = new Node( + {id:nodeId, + mass:1, + shape:'circle', + internalMultiplier:1 + },{},{},this.constants); + edge.via = this.sectors['support']['nodes'][nodeId]; + edge.via.parentEdgeId = edge.id; + edge.positionBezierNode(); } } } @@ -1838,7 +1899,7 @@ Graph.prototype._initializeMixinLoaders = function () { Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction]; } } -} +}; diff --git a/src/graph/Node.js b/src/graph/Node.js index ba86acde..83c372fb 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -34,6 +34,7 @@ function Node(properties, imagelist, grouplist, constants) { this.fontSize = constants.nodes.fontSize; this.fontFace = constants.nodes.fontFace; this.fontColor = constants.nodes.fontColor; + this.fontDrawThreshold = 3; this.color = constants.nodes.color; @@ -54,11 +55,15 @@ function Node(properties, imagelist, grouplist, constants) { this.radiusMax = constants.nodes.radiusMax; this.imagelist = imagelist; - this.grouplist = grouplist; - this.dampingBase = 0.9; - this.damping = 0.9; // this is manipulated in the updateDamping function + // physics properties + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = constants.physics.damping; this.mass = 1; // kg this.setProperties(properties, constants); @@ -73,15 +78,9 @@ function Node(properties, imagelist, grouplist, constants) { this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements; this.growthIndicator = 0; - // mass, force, velocity - - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y - this.minForce = constants.minForce; - + // variables to tell the node about the graph. this.graphScaleInv = 1; + this.graphScale = 1; this.canvasTopLeft = {"x": -300, "y": -300}; this.canvasBottomRight = {"x": 300, "y": 300}; this.parentEdgeId = null; @@ -445,15 +444,7 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - - if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) { -// console.log(vmin,this.vx,this.vy); - return true; - } - else { - return false; - } - //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); }; /** @@ -902,7 +893,7 @@ Node.prototype._drawText = function (ctx) { Node.prototype._label = function (ctx, text, x, y, align, baseline) { - if (text) { + if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) { ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; ctx.fillStyle = this.fontColor || "black"; ctx.textAlign = align || "center"; @@ -956,7 +947,7 @@ Node.prototype.inArea = function() { else { return true; } -} +}; /** * checks if the core of the node is in the display area, this is used for opening clusters around zoom @@ -967,7 +958,7 @@ Node.prototype.inView = function() { this.x < this.canvasBottomRight.x && this.y >= this.canvasTopLeft.y && this.y < this.canvasBottomRight.y); -} +}; /** * This allows the zoom level of the graph to influence the rendering @@ -979,6 +970,7 @@ Node.prototype.inView = function() { */ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { this.graphScaleInv = 1.0/scale; + this.graphScale = scale; this.canvasTopLeft = canvasTopLeft; this.canvasBottomRight = canvasBottomRight; }; @@ -991,17 +983,9 @@ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) */ Node.prototype.setScale = function(scale) { this.graphScaleInv = 1.0/scale; + this.graphScale = scale; }; -/** - * This function updates the damping parameter of the node based on its mass, - * heavier nodes have more damping. - * - * @param {Number} numberOfNodes - */ -Node.prototype.updateDamping = function() { - this.damping = Math.min(Math.max(1.2,this.dampingBase),this.dampingBase + 0.01*this.growthIndicator); -}; /** diff --git a/src/graph/css/graph-manipulation.css b/src/graph/css/graph-manipulation.css new file mode 100644 index 00000000..6d0b41ce --- /dev/null +++ b/src/graph/css/graph-manipulation.css @@ -0,0 +1,128 @@ +div.graph-manipulationDiv { + border-width:0px; + border-bottom: 1px; + border-style:solid; + border-color: #d6d9d8; + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ + + width: 600px; + height:30px; + z-index:10; + position:absolute; +} + +div.graph-manipulation-editMode { + height:30px; + z-index:10; + position:absolute; + margin-top:20px; +} + +div.graph-manipulation-closeDiv { + height:30px; + width:30px; + z-index:11; + position:absolute; + margin-top:3px; + margin-left:590px; + background-position: 0px 0px; + background-repeat:no-repeat; + background-image: url("../../dist/img/cross.png"); + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.graph-manipulationUI { + font-family: verdana; + font-size: 12px; + -moz-border-radius: 15px; + border-radius: 15px; + display:inline-block; + background-position: 0px 0px; + background-repeat:no-repeat; + height:24px; + margin: -14px 0px 0px 10px; + vertical-align:middle; + cursor: pointer; + padding: 0px 8px 0px 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.graph-manipulationUI:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); +} + +span.graph-manipulationUI:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); +} + +span.graph-manipulationUI.back { + background-image: url("../../dist/img/backIcon.png"); +} + +span.graph-manipulationUI.none:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); + cursor: default; +} +span.graph-manipulationUI.none:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); +} +span.graph-manipulationUI.none { + padding: 0px 0px 0px 0px; +} +span.graph-manipulationUI.notification{ + margin: 2px; + font-weight: bold; +} + +span.graph-manipulationUI.add { + background-image: url("../../dist/img/addNodeIcon.png"); +} + +span.graph-manipulationUI.edit { + background-image: url("../../dist/img/editIcon.png"); +} + +span.graph-manipulationUI.edit.editmode { + background-color: #fcfcfc; + border-style:solid; + border-width:1px; + border-color: #cccccc; +} + +span.graph-manipulationUI.connect { + background-image: url("../../dist/img/connectIcon.png"); +} + +span.graph-manipulationUI.delete { + background-image: url("../../dist/img/deleteIcon.png"); +} +/* top right bottom left */ +span.graph-manipulationLabel { + margin: 0px 0px 0px 23px; + line-height: 25px; +} +div.graph-seperatorLine { + display:inline-block; + width:1px; + height:20px; + background-color: #bdbdbd; + margin: 5px 7px 0px 15px; +} \ No newline at end of file diff --git a/src/graph/css/graph-navigation.css b/src/graph/css/graph-navigation.css new file mode 100644 index 00000000..f72c496a --- /dev/null +++ b/src/graph/css/graph-navigation.css @@ -0,0 +1,62 @@ +div.graph-navigation { + width:34px; + height:34px; + z-index:10; + -moz-border-radius: 17px; + border-radius: 17px; + position:absolute; + display:inline-block; + background-position: 2px 2px; + background-repeat:no-repeat; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.graph-navigation:hover { + box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30); +} + +div.graph-navigation:active { + box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95); +} + +div.graph-navigation.up { + background-image: url("../../dist/img/upArrow.png"); + margin-top:520px; + margin-left:55px; +} +div.graph-navigation.down { + background-image: url("../../dist/img/downArrow.png"); + margin-top:560px; + margin-left:55px; +} +div.graph-navigation.left { + background-image: url("../../dist/img/leftArrow.png"); + margin-top:560px; + margin-left:15px; +} +div.graph-navigation.right { + background-image: url("../../dist/img/rightArrow.png"); + margin-top:560px; + margin-left:95px; +} +div.graph-navigation.zoomIn { + background-image: url("../../dist/img/plus.png"); + margin-top:560px; + margin-left:555px; +} +div.graph-navigation.zoomOut { + background-image: url("../../dist/img/minus.png"); + margin-top:560px; + margin-left:515px; +} +div.graph-navigation.zoomExtends { + background-image: url("../../dist/img/zoomExtends.png"); + margin-top:520px; + margin-left:555px; +} \ No newline at end of file diff --git a/src/graph/graphMixins/ClusterMixin.js b/src/graph/graphMixins/ClusterMixin.js index b84dd84f..0ca8eb5f 100644 --- a/src/graph/graphMixins/ClusterMixin.js +++ b/src/graph/graphMixins/ClusterMixin.js @@ -57,6 +57,7 @@ var ClusterMixin = { if (level > 0 && reposition == true) { this.repositionNodes(); } + this._updateCalculationNodes(); }, /** @@ -86,7 +87,7 @@ var ClusterMixin = { // update the index list, dynamic edges and labels this._updateNodeIndexList(); this._updateDynamicEdges(); - this._setCalculationNodes(); + this._updateCalculationNodes(); this.updateLabels(); } @@ -198,7 +199,7 @@ var ClusterMixin = { } } - this._setCalculationNodes(); + this._updateCalculationNodes(); }, /** @@ -283,7 +284,7 @@ var ClusterMixin = { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; this._expandClusterNode(node,recursive,force); - this._setCalculationNodes(); + this._updateCalculationNodes(); } }, @@ -1044,7 +1045,7 @@ var ClusterMixin = { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) { - var radius = this.constants.physics.springLength * (1 + 0.1*node.mass); + var radius = this.constants.physics.springLength * node.mass; var angle = 2 * Math.PI * Math.random(); if (node.xFixed == false) {node.x = radius * Math.cos(angle);} if (node.yFixed == false) {node.y = radius * Math.sin(angle);} diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/graph/graphMixins/ManipulationMixin.js index 7e734f68..d1dfc0c1 100644 --- a/src/graph/graphMixins/ManipulationMixin.js +++ b/src/graph/graphMixins/ManipulationMixin.js @@ -15,7 +15,13 @@ var manipulationMixin = { } }, - + /** + * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore + * these functions to their original functionality, we saved them in this.cachedFunctions. + * This function restores these functions to their original function. + * + * @private + */ _restoreOverloadedFunctions : function() { for (var functionName in this.cachedFunctions) { if (this.cachedFunctions.hasOwnProperty(functionName)) { @@ -24,15 +30,27 @@ var manipulationMixin = { } }, - + /** + * Enable or disable edit-mode. + * + * @private + */ _toggleEditMode : function() { this.editMode = !this.editMode; - var toolbar = document.getElementById("graph-manipulationDiv") + var toolbar = document.getElementById("graph-manipulationDiv"); + var closeDiv = document.getElementById("graph-manipulation-closeDiv"); + var editModeDiv = document.getElementById("graph-manipulation-editMode"); if (this.editMode == true) { toolbar.style.display="block"; + closeDiv.style.display="block"; + editModeDiv.style.display="none"; + closeDiv.onclick = this._toggleEditMode.bind(this); } else { toolbar.style.display="none"; + closeDiv.style.display="none"; + editModeDiv.style.display="block"; + closeDiv.onclick = null; } this._createManipulatorBar() }, @@ -62,37 +80,51 @@ var manipulationMixin = { } // add the icons to the manipulator div this.manipulationDiv.innerHTML = "" + - "Add Node" + - "
    " + - "Add Link"; + "" + + "Add Node" + + "
    " + + "" + + "Add Link"; if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { this.manipulationDiv.innerHTML += "" + - "
    " + - "Edit Node"; + "
    " + + "" + + "Edit Node"; } if (this._selectionIsEmpty() == false) { this.manipulationDiv.innerHTML += "" + - "
    " + - "Delete selected"; + "
    " + + "" + + "Delete selected"; } // bind the icons - var addNodeButton = document.getElementById("manipulate-addNode"); + var addNodeButton = document.getElementById("graph-manipulate-addNode"); addNodeButton.onclick = this._createAddNodeToolbar.bind(this); - var addEdgeButton = document.getElementById("manipulate-connectNode"); + var addEdgeButton = document.getElementById("graph-manipulate-connectNode"); addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - var editButton = document.getElementById("manipulate-editNode"); + var editButton = document.getElementById("graph-manipulate-editNode"); editButton.onclick = this._editNode.bind(this); } if (this._selectionIsEmpty() == false) { - var deleteButton = document.getElementById("manipulate-delete"); + var deleteButton = document.getElementById("graph-manipulate-delete"); deleteButton.onclick = this._deleteSelected.bind(this); } + var closeDiv = document.getElementById("graph-manipulation-closeDiv"); + closeDiv.onclick = this._toggleEditMode.bind(this); + this.boundFunction = this._createManipulatorBar.bind(this); this.on('select', this.boundFunction); } + else { + this.editModeDiv.innerHTML = "" + + "" + + "Edit" + var editModeButton = document.getElementById("graph-manipulate-editModeButton"); + editModeButton.onclick = this._toggleEditMode.bind(this); + } }, @@ -109,12 +141,14 @@ var manipulationMixin = { // create the toolbar contents this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Click in an empty space to place a new node"; + "" + + "Back" + + "
    " + + "" + + "Click in an empty space to place a new node"; // bind the icon - var backButton = document.getElementById("manipulate-back"); + var backButton = document.getElementById("graph-manipulate-back"); backButton.onclick = this._createManipulatorBar.bind(this); // we use the boundFunction so we can reference it when we unbind it from the "select" event. @@ -141,12 +175,14 @@ var manipulationMixin = { this.blockConnectingEdgeSelection = true; this.manipulationDiv.innerHTML = "" + - "Back" + - "
    " + - "Click on a node and drag the edge to another node."; + "" + + "Back" + + "
    " + + "" + + "Click on a node and drag the edge to another node to connect them."; // bind the icon - var backButton = document.getElementById("manipulate-back"); + var backButton = document.getElementById("graph-manipulate-back"); backButton.onclick = this._createManipulatorBar.bind(this); // we use the boundFunction so we can reference it when we unbind it from the "select" event. @@ -273,7 +309,6 @@ var manipulationMixin = { } } else { - console.log("didnt use funciton") this.createNodeOnClick = true; this.nodesData.add(defaultData); this.createNodeOnClick = false; @@ -373,6 +408,7 @@ var manipulationMixin = { this.triggerFunctions.delete(data, function (finalizedData) { me.edgesData.remove(finalizedData.edges); me.nodesData.remove(finalizedData.nodes); + this._unselectAll(); me.moving = true; me.start(); }); @@ -384,6 +420,7 @@ var manipulationMixin = { else { this.edgesData.remove(selectedEdges); this.nodesData.remove(selectedNodes); + this._unselectAll(); this.moving = true; this.start(); } diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js index 325febf9..f025b08a 100644 --- a/src/graph/graphMixins/MixinLoader.js +++ b/src/graph/graphMixins/MixinLoader.js @@ -59,6 +59,7 @@ var graphMixinLoaders = { 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.constants.physics.springGrowthPerMass = this.constants.physics.barnesHut.springGrowthPerMass; this._loadMixin(barnesHutMixin); @@ -70,6 +71,7 @@ var graphMixinLoaders = { 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.constants.physics.springGrowthPerMass = this.constants.physics.repulsion.springGrowthPerMass; this._loadMixin(repulsionMixin); @@ -103,12 +105,7 @@ var graphMixinLoaders = { "nodeIndices":[], "formationScale": 1.0, "drawingNode": undefined }; - this.sectors["frozen"] = { }, - this.sectors["navigation"] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined }; + this.sectors["frozen"] = {}, this.sectors["support"] = {"nodes":{}, "edges":{}, "nodeIndices":[], @@ -143,8 +140,7 @@ var graphMixinLoaders = { this.blockConnectingEdgeSelection = false; this.forceAppendSelection = false - - if (this.constants.dataManipulationToolbar.enabled == true) { + if (this.constants.dataManipulation.enabled == true) { // load the manipulator HTML elements. All styling done in css. if (this.manipulationDiv === undefined) { this.manipulationDiv = document.createElement('div'); @@ -158,6 +154,28 @@ var graphMixinLoaders = { } this.containerElement.insertBefore(this.manipulationDiv, this.frame); } + + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement('div'); + this.editModeDiv.className = 'graph-manipulation-editMode'; + this.editModeDiv.id = 'graph-manipulation-editMode'; + if (this.editMode == true) { + this.editModeDiv.style.display = "none"; + } + else { + this.editModeDiv.style.display = "block"; + } + this.containerElement.insertBefore(this.editModeDiv, this.frame); + } + + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement('div'); + this.closeDiv.className = 'graph-manipulation-closeDiv'; + this.closeDiv.id = 'graph-manipulation-closeDiv'; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.containerElement.insertBefore(this.closeDiv, this.frame); + } + // load the manipulation functions this._loadMixin(manipulationMixin); @@ -166,9 +184,17 @@ var graphMixinLoaders = { } else { if (this.manipulationDiv !== undefined) { + // removes all the bindings and overloads this._createManipulatorBar(); + // remove the manipulation divs this.containerElement.removeChild(this.manipulationDiv); + this.containerElement.removeChild(this.editModeDiv); + this.containerElement.removeChild(this.closeDiv); + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + // remove the mixin functions this._clearMixin(manipulationMixin); } } @@ -183,24 +209,11 @@ var graphMixinLoaders = { _loadNavigationControls : function() { this._loadMixin(NavigationMixin); + // the clean function removes the button divs, this is done to remove the bindings. + this._cleanNavigation(); if (this.constants.navigation.enabled == true) { this._loadNavigationElements(); } - }, - - - /** - * this function exists to avoid errors when not loading the navigation system - */ - _relocateNavigation : function() { - // empty, is overloaded by navigation system - }, + } - - /** - * this function exists to avoid errors when not loading the navigation system - */ - _unHighlightAll : function() { - // empty, is overloaded by the navigation system - } } diff --git a/src/graph/graphMixins/NavigationMixin.js b/src/graph/graphMixins/NavigationMixin.js index 20540622..60897987 100644 --- a/src/graph/graphMixins/NavigationMixin.js +++ b/src/graph/graphMixins/NavigationMixin.js @@ -4,35 +4,15 @@ var NavigationMixin = { - /** - * This function moves the navigation controls if the canvas size has been changed. If the arugments - * verticaAlignTop and horizontalAlignLeft are false, the correction will be made - * - * @private - */ - _relocateNavigation : function() { - if (this.sectors !== undefined) { - var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth; - var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - var node = null; - - for (var nodeId in this.sectors["navigation"]["nodes"]) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) { - node = this.sectors["navigation"]["nodes"][nodeId]; - if (!node.horizontalAlignLeft) { - node.x -= xOffset; - } - if (!node.verticalAlignTop) { - node.y -= yOffset; - } - } - } + _cleanNavigation : function() { + // clean up previosu navigation items + var wrapper = document.getElementById('graph-navigation_wrapper'); + if (wrapper != null) { + this.containerElement.removeChild(wrapper); } + document.onmouseup = null; }, - /** * 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 @@ -42,83 +22,45 @@ var NavigationMixin = { * @private */ _loadNavigationElements : function() { - var DIR = this.constants.navigation.iconPath; - this.navigationClientWidth = this.frame.canvas.clientWidth; - this.navigationClientHeight = this.frame.canvas.clientHeight; - if (this.navigationClientWidth === undefined) { - this.navigationClientWidth = 0; - this.navigationClientHeight = 0; + this._cleanNavigation(); + + this.navigationDivs = {}; + var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; + var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomToFit']; + + this.navigationDivs['wrapper'] = document.createElement('div'); + this.navigationDivs['wrapper'].id = "graph-navigation_wrapper"; + this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDivs[navigationDivs[i]] = document.createElement('div'); + this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i]; + this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i]; + this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); + this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); } - var offset = 15; - var intermediateOffset = 7; - var navigationNodes = [ - {id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}, - {id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown", - verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft", - verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight", - verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - - {id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset}, - {id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit", - verticalAlignTop: false, horizontalAlignLeft: false, - x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset} - ]; - - var nodeObj = null; - for (var i = 0; i < navigationNodes.length; i++) { - nodeObj = this.sectors["navigation"]['nodes']; - nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants); - } - }, + document.onmouseup = this._stopMovement.bind(this); + }, /** - * By setting the clustersize to be larger than 1, we use the clustering drawing method - * to illustrate the buttons are presed. We call this highlighting. + * this stops all movement induced by the navigation buttons * - * @param {String} elementId * @private */ - _highlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; - } + _stopMovement : function() { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); }, /** - * Reverting back to a normal button + * stops the actions performed by page up and down etc. * - * @param {String} elementId - * @private - */ - _unHighlightNavigationElement : function(elementId) { - if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { - this.sectors["navigation"]["nodes"][elementId].clusterSize = 1; - } - }, - - /** - * un-highlight (for lack of a better term) all navigation controls elements + * @param event * @private */ - _unHighlightAll : function() { - for (var nodeId in this.sectors['navigation']['nodes']) { - if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) { - this._unHighlightNavigationElement(nodeId); - } - } - }, - - _preventDefault : function(event) { if (event !== undefined) { if (event.preventDefault) { @@ -139,7 +81,7 @@ var NavigationMixin = { * @private */ _moveUp : function(event) { - this._highlightNavigationElement("navigation_up"); + console.log("here") this.yIncrement = this.constants.keyboard.speed.y; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -151,7 +93,6 @@ var NavigationMixin = { * @private */ _moveDown : function(event) { - this._highlightNavigationElement("navigation_down"); this.yIncrement = -this.constants.keyboard.speed.y; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -163,7 +104,6 @@ var NavigationMixin = { * @private */ _moveLeft : function(event) { - this._highlightNavigationElement("navigation_left"); this.xIncrement = this.constants.keyboard.speed.x; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -175,7 +115,6 @@ var NavigationMixin = { * @private */ _moveRight : function(event) { - this._highlightNavigationElement("navigation_right"); this.xIncrement = -this.constants.keyboard.speed.y; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -187,7 +126,6 @@ var NavigationMixin = { * @private */ _zoomIn : function(event) { - this._highlightNavigationElement("navigation_plus"); this.zoomIncrement = this.constants.keyboard.speed.zoom; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -199,7 +137,6 @@ var NavigationMixin = { * @private */ _zoomOut : function() { - this._highlightNavigationElement("navigation_min"); this.zoomIncrement = -this.constants.keyboard.speed.zoom; this.start(); // if there is no node movement, the calculation wont be done this._preventDefault(event); @@ -211,9 +148,6 @@ var NavigationMixin = { * @private */ _stopZoom : function() { - this._unHighlightNavigationElement("navigation_plus"); - this._unHighlightNavigationElement("navigation_min"); - this.zoomIncrement = 0; }, @@ -223,9 +157,6 @@ var NavigationMixin = { * @private */ _yStopMoving : function() { - this._unHighlightNavigationElement("navigation_up"); - this._unHighlightNavigationElement("navigation_down"); - this.yIncrement = 0; }, @@ -235,9 +166,6 @@ var NavigationMixin = { * @private */ _xStopMoving : function() { - this._unHighlightNavigationElement("navigation_left"); - this._unHighlightNavigationElement("navigation_right"); - this.xIncrement = 0; } diff --git a/src/graph/graphMixins/SectorsMixin.js b/src/graph/graphMixins/SectorsMixin.js index 1661c28c..331586ef 100644 --- a/src/graph/graphMixins/SectorsMixin.js +++ b/src/graph/graphMixins/SectorsMixin.js @@ -84,19 +84,6 @@ var SectorMixin = { }, - /** - * This function sets the global references to nodes, edges and nodeIndices to - * those of the navigation controls sector. - * - * @private - */ - _switchToNavigationSector : function() { - this.nodeIndices = this.sectors["navigation"]["nodeIndices"]; - this.nodes = this.sectors["navigation"]["nodes"]; - this.edges = this.sectors["navigation"]["edges"]; - }, - - /** * This function sets the global references to nodes, edges and nodeIndices back to * those of the currently active sector. @@ -365,7 +352,7 @@ var SectorMixin = { this._updateNodeIndexList(); // we refresh the list with calulation nodes and calculation node indices. - this._setCalculationNodes(); + this._updateCalculationNodes(); } } }, @@ -477,33 +464,6 @@ var SectorMixin = { }, - /** - * This runs a function in the navigation controls sector. - * - * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors - * | we don't pass the function itself because then the "this" is the window object - * | instead of the Graph object - * @param {*} [argument] | Optional: arguments to pass to the runFunction - * @private - */ - _doInNavigationSector : function(runFunction,argument) { - this._switchToNavigationSector(); - if (argument === undefined) { - this[runFunction](); - } - else { - var args = Array.prototype.splice.call(arguments, 1); - if (args.length > 1) { - this[runFunction](args[0],args[1]); - } - else { - this[runFunction](argument); - } - } - this._loadLatestSector(); - }, - - /** * This runs a function in all sectors. This is used in the _redraw(). * diff --git a/src/graph/graphMixins/SelectionMixin.js b/src/graph/graphMixins/SelectionMixin.js index 01078805..01b59511 100644 --- a/src/graph/graphMixins/SelectionMixin.js +++ b/src/graph/graphMixins/SelectionMixin.js @@ -32,18 +32,6 @@ var SelectionMixin = { }, - /** - * retrieve all nodes in the navigation controls overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllNavigationNodesOverlappingWith : function (object) { - var overlappingNodes = []; - this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes); - return overlappingNodes; - }, - /** * Return a position object in canvasspace from a single point in screenspace * @@ -61,42 +49,6 @@ var SelectionMixin = { bottom: y}; }, - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - _pointerToScreenPositionObject : function(pointer) { - var x = pointer.x; - var y = pointer.y; - - return {left: x, - top: y, - right: x, - bottom: y}; - }, - - - /** - * Get the top navigation controls node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - _getNavigationNodeAt : function (pointer) { - var screenPositionObject = this._pointerToScreenPositionObject(pointer); - var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject); - if (overlappingNodes.length > 0) { - return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]]; - } - else { - return null; - } - }, - /** * Get the top node at the a specific point (like a click) @@ -426,15 +378,7 @@ var SelectionMixin = { * @private */ _handleTouch : function(pointer) { - if (this.constants.navigation.enabled == true) { - this.pointerPosition = pointer; - var node = this._getNavigationNodeAt(pointer); - if (node != null) { - if (this[node.triggerFunction] !== undefined) { - this[node.triggerFunction](); - } - } - } + }, @@ -506,10 +450,7 @@ var SelectionMixin = { * @private */ _handleOnRelease : function(pointer) { - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; - this._unHighlightAll(); + }, diff --git a/src/graph/graphMixins/physics/PhysicsMixin.js b/src/graph/graphMixins/physics/PhysicsMixin.js index 4fb4284a..c5513761 100644 --- a/src/graph/graphMixins/physics/PhysicsMixin.js +++ b/src/graph/graphMixins/physics/PhysicsMixin.js @@ -72,7 +72,7 @@ var physicsMixin = { * * @private */ - _setCalculationNodes : function() { + _updateCalculationNodes : function() { if (this.constants.smoothCurves == true) { this.calculationNodes = {}; this.calculationNodeIndices = []; @@ -113,27 +113,28 @@ var physicsMixin = { * @private */ _calculateGravitationalForces : function() { - var dx, dy, angle, fx, fy, node, i; + var dx, dy, distance, node, i; var nodes = this.calculationNodes; var gravity = this.constants.physics.centralGravity; + var gravityForce = 0; 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") { dx = -node.x; dy = -node.y; + distance = Math.sqrt(dx*dx + dy*dy); + gravityForce = gravity / distance; - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; + node.fx = dx * gravityForce; + node.fy = dy * gravityForce; } else { - fx = 0; - fy = 0; + node.fx = 0; + node.fy = 0; } - node._setForce(fx, fy); - node.updateDamping(); } }, @@ -145,6 +146,7 @@ var physicsMixin = { */ _calculateSpringForces : function() { var edgeLength, edge, edgeId; + var dx, dy, fx, fy, springForce, length; var edges = this.edges; // forces caused by the edges, modelled as springs @@ -157,7 +159,20 @@ var physicsMixin = { edgeLength = edge.length; // this implies that the edges between big clusters are longer edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; - this._calculateSpringForce(edge.from,edge.to,edgeLength); + + dx = (edge.from.x - edge.to.x); + dy = (edge.from.y - edge.to.y); + length = Math.sqrt(dx * dx + dy * dy); + + springForce = this.constants.physics.springConstant * (edgeLength - length) / length; + + fx = dx * springForce; + fy = dy * springForce; + + edge.from.fx += fx; + edge.from.fy += fy; + edge.to.fx -= fx; + edge.to.fy -= fy; } } } @@ -211,19 +226,20 @@ var physicsMixin = { * @private */ _calculateSpringForce : function(node1,node2,edgeLength) { - var dx, dy, angle, fx, fy, springForce, length; + var dx, dy, fx, fy, springForce, length; dx = (node1.x - node2.x); dy = (node1.y - node2.y); length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); - springForce = this.constants.physics.springConstant * (edgeLength - length); + springForce = this.constants.physics.springConstant * (edgeLength - length) / length; - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; + fx = dx * springForce; + fy = dy * springForce; - node1._addForce(fx, fy); - node2._addForce(-fx, -fy); + node1.fx += fx; + node1.fy += fy; + node2.fx -= fx; + node2.fy -= fy; } } \ No newline at end of file diff --git a/src/graph/graphMixins/physics/barnesHut.js b/src/graph/graphMixins/physics/barnesHut.js index a7646213..9e658c8c 100644 --- a/src/graph/graphMixins/physics/barnesHut.js +++ b/src/graph/graphMixins/physics/barnesHut.js @@ -50,49 +50,47 @@ var barnesHutMixin = { dy = parentBranch.centerOfMass.y - node.y; distance = Math.sqrt(dx * dx + dy * dy); - if (distance > 0) { // distance is 0 if it looks to apply a force on itself. - // BarnesHut condition - if (distance * parentBranch.calcSize < this.constants.physics.barnesHut.theta) { - // 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 - this._getForceOnNode(parentBranch, node, dx ,dy, distance); + // BarnesHut condition + // original condition : s/d < theta = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) { + // 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.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.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; } } - else { - this._getForceOnNode(parentBranch, node, dx ,dy, distance); - } } } }, - /** - * The gravitational force applied on the node by the mass of the branch. - * - * @param parentBranch - * @param node - * @param dx - * @param dy - * @param distance - * @private - */ - _getForceOnNode : function(parentBranch, node, dx ,dy, distance) { - // even if the parentBranch only has one node, its Center of Mass is at the right place (the node in this case). - var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance); - var angle = Math.atan2(dy, dx); - var fx = Math.cos(angle) * gravityForce; - var fy = Math.sin(angle) * gravityForce; - node._addForce(fx, fy); - }, - - /** * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. * @@ -177,7 +175,7 @@ var barnesHutMixin = { // update the mass of the branch. this._updateBranchMass(parentBranch,node); } - //console.log(parentBranch.children.NW.range.maxX,parentBranch.children.NW.range.maxY, node.x,node.y); + 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"); @@ -209,7 +207,9 @@ var barnesHutMixin = { // 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 += 0.1; + node.x += Math.random(); + node.y += Math.random(); + this._placeInTree(parentBranch,node, true); } else { this._splitBranch(parentBranch.children[region]); diff --git a/src/graph/graphMixins/physics/repulsion.js b/src/graph/graphMixins/physics/repulsion.js index c1bc805f..dccfb9c4 100644 --- a/src/graph/graphMixins/physics/repulsion.js +++ b/src/graph/graphMixins/physics/repulsion.js @@ -41,8 +41,6 @@ var repulsionMixin = { minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); var a = a_base / minimumDistance; if (distance < 2*minimumDistance) { - angle = Math.atan2(dy, dx); - if (distance < 0.5*minimumDistance) { repulsingForce = 1.0; } @@ -50,17 +48,17 @@ var repulsionMixin = { repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) } - if (this.sectors['support']['nodes'].hasOwnProperty(node1.id)) { - } - // amplify the repulsion for clusters. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; + repulsingForce = repulsingForce/distance; - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; + fx = dx * repulsingForce; + fy = dy * repulsingForce; - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); + node1.fx -= fx; + node1.fy -= fy; + node2.fx += fx; + node2.fy += fy; } } } diff --git a/src/graph/img/cross.png b/src/graph/img/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..9cbd189ab6ae99ed5794c30a12ddc0480987f29f GIT binary patch literal 18303 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 literal 0 HcmV?d00001