Browse Source

refactoring, altered data manipulation, added callbacks and triggers, inserted temporary function overloading

css_transitions
Alex de Mulder 10 years ago
parent
commit
d3506a82b3
11 changed files with 439 additions and 417 deletions
  1. +0
    -1
      examples/graph/02_random_nodes.html
  2. +23
    -8
      examples/graph/21_data_manipulation.html
  3. +1
    -0
      examples/graph/index.html
  4. +37
    -23
      src/graph/Edge.js
  5. +64
    -57
      src/graph/Graph.js
  6. +25
    -8
      src/graph/Node.js
  7. +248
    -317
      src/graph/graphMixins/ManipulationMixin.js
  8. +15
    -0
      src/graph/graphMixins/MixinLoader.js
  9. +18
    -1
      src/graph/graphMixins/SelectionMixin.js
  10. +4
    -2
      src/graph/graphMixins/physics/repulsion.js
  11. +4
    -0
      src/util.js

+ 0
- 1
examples/graph/02_random_nodes.html View File

@ -88,7 +88,6 @@
*/ */
var options = { var options = {
edges: { edges: {
}, },
stabilize: false stabilize: false
}; };

+ 23
- 8
examples/graph/21_data_manipulation.html View File

@ -38,13 +38,14 @@
border-style:solid; border-style:solid;
border-color: #d6d9d8; border-color: #d6d9d8;
background: #ffffff; /* Old browsers */ background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #f3f3f3 50%, #ededed 51%, #ffffff 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(50%,#f3f3f3), color-stop(51%,#ededed), color-stop(100%,#ffffff)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#f3f3f3 50%,#ededed 51%,#ffffff 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */
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; width: 600px;
height:30px; height:30px;
z-index:10; z-index:10;
@ -223,7 +224,21 @@
clustering:false, clustering:false,
navigation: true, navigation: true,
keyboard: true, keyboard: true,
dataManipulationToolbar: true
dataManipulationToolbar: true,
triggerFunctions: {add: function(data,callback) {
data.label = "hello";
callback(data);
},
edit: function(data,callback) {
data.label='edited'
callback(data);
},
edit: function(data,callback) {
data.label='edited'
callback(data);
}
}
}; };
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);

+ 1
- 0
examples/graph/index.html View File

@ -33,6 +33,7 @@
<p><a href="19_scale_free_graph_clustering.html">19_scale_free_graph_clustering.html</a></p> <p><a href="19_scale_free_graph_clustering.html">19_scale_free_graph_clustering.html</a></p>
<p><a href="20_navigation.html">20_navigation.html</a></p> <p><a href="20_navigation.html">20_navigation.html</a></p>
<p><a href="21_data_manipulation.html">21_data_manipulation.html</a></p> <p><a href="21_data_manipulation.html">21_data_manipulation.html</a></p>
<p><a href="22_les_miserables.html">22_les_miserables.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p> <p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div> </div>

+ 37
- 23
src/graph/Edge.js View File

@ -212,8 +212,7 @@ Edge.prototype.isOverlappingWith = function(obj) {
var xObj = obj.left; var xObj = obj.left;
var yObj = obj.top; var yObj = obj.top;
var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
return (dist < distMax); return (dist < distMax);
}; };
@ -651,31 +650,46 @@ Edge.prototype._drawArrow = function(ctx) {
* @param {number} y3 * @param {number} y3
* @private * @private
*/ */
Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
if (this.smooth == true) {
var minDistance = 1e9;
var i,t,x,y,dx,dy;
for (i = 0; i < 10; i++) {
t = 0.1*i;
x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
dx = Math.abs(x3-x);
dy = Math.abs(y3-y);
minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
}
return minDistance
} }
else {
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
}
//# 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
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
return Math.sqrt(dx*dx + dy*dy);
//# 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
return Math.sqrt(dx*dx + dy*dy);
}
}; };

+ 64
- 57
src/graph/Graph.js View File

@ -22,6 +22,8 @@ function Graph (container, data, options) {
this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
this.stabilize = true; // stabilize before displaying the graph this.stabilize = true; // stabilize before displaying the graph
this.selectable = true; this.selectable = true;
// these functions can be triggered when the dataset is edited
this.triggerFunctions = {add:null,edit:null,connect:null,delete:null};
// set constant values // set constant values
this.constants = { this.constants = {
@ -29,7 +31,6 @@ function Graph (container, data, options) {
radiusMin: 5, radiusMin: 5,
radiusMax: 20, radiusMax: 20,
radius: 5, radius: 5,
distance: 100, // px
shape: 'ellipse', shape: 'ellipse',
image: undefined, image: undefined,
widthMin: 16, // px widthMin: 16, // px
@ -117,13 +118,15 @@ function Graph (container, data, options) {
speed: {x: 10, y: 10, zoom: 0.02} speed: {x: 10, y: 10, zoom: 0.02}
}, },
dataManipulationToolbar: { dataManipulationToolbar: {
enabled: false
enabled: false,
initiallyVisible: false
}, },
smoothCurves: true, smoothCurves: true,
maxVelocity: 25, maxVelocity: 25,
minVelocity: 0.1, // px/s minVelocity: 0.1, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
}; };
this.editMode = this.constants.dataManipulationToolbar.initiallyVisible;
// Node variables // Node variables
this.groups = new Groups(); // object with groups this.groups = new Groups(); // object with groups
@ -158,7 +161,7 @@ function Graph (container, data, options) {
// other vars // other vars
var graph = this; var graph = this;
this.freezeSimulation = false;// freeze the simulation this.freezeSimulation = false;// freeze the simulation
this.cachedFunctions = {};
this.calculationNodes = {}; this.calculationNodes = {};
this.calculationNodeIndices = []; this.calculationNodeIndices = [];
@ -416,6 +419,12 @@ Graph.prototype.setOptions = function (options) {
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;} if (options.selectable !== undefined) {this.selectable = options.selectable;}
if (options.triggerFunctions) {
for (prop in options.triggerFunctions) {
this.triggerFunctions[prop] = options.triggerFunctions[prop];
}
}
if (options.physics) { if (options.physics) {
if (options.physics.barnesHut) { if (options.physics.barnesHut) {
this.constants.physics.barnesHut.enabled = true; this.constants.physics.barnesHut.enabled = true;
@ -553,6 +562,7 @@ Graph.prototype.setOptions = function (options) {
this.setSize(this.width, this.height); this.setSize(this.width, this.height);
this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
this._setScale(1); this._setScale(1);
this.zoomToFit()
this._redraw(); this._redraw();
}; };
@ -687,6 +697,8 @@ Graph.prototype._createKeyBinds = function() {
if (this.constants.dataManipulationToolbar.enabled == true) { if (this.constants.dataManipulationToolbar.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
this.mousetrap.bind("del",this._deleteSelected.bind(me));
this.mousetrap.bind("e",this._toggleEditMode.bind(me));
} }
} }
@ -721,6 +733,11 @@ Graph.prototype._onTouch = function (event) {
* @private * @private
*/ */
Graph.prototype._onDragStart = function () { Graph.prototype._onDragStart = function () {
this._handleDragStart();
};
Graph.prototype._handleDragStart = function() {
var drag = this.drag; var drag = this.drag;
var node = this._getNodeAt(drag.pointer); var node = this._getNodeAt(drag.pointer);
// note: drag.pointer is set in _onTouch to get the initial touch location // note: drag.pointer is set in _onTouch to get the initial touch location
@ -761,13 +778,18 @@ Graph.prototype._onDragStart = function () {
} }
} }
} }
};
}
/** /**
* handle drag event * handle drag event
* @private * @private
*/ */
Graph.prototype._onDrag = function (event) { Graph.prototype._onDrag = function (event) {
this._handleOnDrag(event)
};
Graph.prototype._handleOnDrag = function(event) {
if (this.drag.pinched) { if (this.drag.pinched) {
return; return;
} }
@ -775,12 +797,12 @@ Graph.prototype._onDrag = function (event) {
var pointer = this._getPointer(event.gesture.touches[0]); var pointer = this._getPointer(event.gesture.touches[0]);
var me = this, var me = this,
drag = this.drag,
selection = drag.selection;
drag = this.drag,
selection = drag.selection;
if (selection && selection.length) { if (selection && selection.length) {
// calculate delta's and new location // calculate delta's and new location
var deltaX = pointer.x - drag.pointer.x, var deltaX = pointer.x - drag.pointer.x,
deltaY = pointer.y - drag.pointer.y;
deltaY = pointer.y - drag.pointer.y;
// update position of all selected nodes // update position of all selected nodes
selection.forEach(function (s) { selection.forEach(function (s) {
@ -807,12 +829,12 @@ Graph.prototype._onDrag = function (event) {
var diffY = pointer.y - this.drag.pointer.y; var diffY = pointer.y - this.drag.pointer.y;
this._setTranslation( this._setTranslation(
this.drag.translation.x + diffX,
this.drag.translation.y + diffY);
this.drag.translation.x + diffX,
this.drag.translation.y + diffY);
this._redraw(); this._redraw();
this.moved = true; this.moved = true;
} }
};
}
/** /**
* handle drag start event * handle drag start event
@ -869,7 +891,8 @@ Graph.prototype._onHold = function (event) {
* @private * @private
*/ */
Graph.prototype._onRelease = function (event) { Graph.prototype._onRelease = function (event) {
this._handleOnRelease();
var pointer = this._getPointer(event.gesture.touches[0]);
this._handleOnRelease(pointer);
}; };
/** /**
@ -1188,6 +1211,7 @@ Graph.prototype._addNodes = function(ids) {
} }
} }
this._updateNodeIndexList(); this._updateNodeIndexList();
this._setCalculationNodes()
this._reconnectEdges(); this._reconnectEdges();
this._updateValueRange(this.nodes); this._updateValueRange(this.nodes);
this.updateLabels(); this.updateLabels();
@ -1710,7 +1734,6 @@ Graph.prototype._discreteStepNodes = function() {
*/ */
Graph.prototype.start = function() { Graph.prototype.start = function() {
if (!this.freezeSimulation) { if (!this.freezeSimulation) {
if (this.moving) { if (this.moving) {
this._doInAllActiveSectors("_initializeForceCalculation"); this._doInAllActiveSectors("_initializeForceCalculation");
if (this.constants.smoothCurves) { if (this.constants.smoothCurves) {
@ -1719,55 +1742,39 @@ Graph.prototype.start = function() {
this._doInAllActiveSectors("_discreteStepNodes"); this._doInAllActiveSectors("_discreteStepNodes");
this._findCenter(this._getRange()) 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);
}
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;
// var calctimeStart = Date.now();
graph.start();
graph.start();
// var calctime = Date.now() - calctimeStart;
// var rendertimeStart = Date.now();
graph._redraw();
// var rendertime = Date.now() - rendertimeStart;
// this.end = window.performance.now();
// this.time = this.end - this.startTime;
// console.log('refresh time: ' + this.time);
// this.startTime = window.performance.now();
// var DOMelement = document.getElementById("calctimereporter");
// if (DOMelement !== undefined) {
// DOMelement.innerHTML = calctime;
// }
// DOMelement = document.getElementById("rendertimereporter");
// if (DOMelement !== undefined) {
// DOMelement.innerHTML = rendertime;
// }
}, this.renderTimestep);
}
}
else {
this._redraw();
// 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);
}
graph.start();
graph.start();
graph._redraw();
}, this.renderTimestep);
} }
} }
else {
this._redraw();
}
}; };
/** /**

+ 25
- 8
src/graph/Node.js View File

@ -228,15 +228,32 @@ Node.prototype.setProperties = function(properties, constants) {
Node.parseColor = function(color) { Node.parseColor = function(color) {
var c; var c;
if (util.isString(color)) { if (util.isString(color)) {
c = {
border: color,
background: color,
highlight: {
if (util.isValidHex(color)) {
var hsv = util.hexToHSV(color);
var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
c = {
border: color, border: color,
background: color
}
};
// TODO: automatically generate a nice highlight color
border:darkerColorHex,
highlight: {
background:lighterColorHex,
border:darkerColorHex
}
};
}
else {
c = {
border:color,
border:color,
highlight: {
background:color,
border:color
}
};
}
} }
else { else {
c = {}; c = {};

+ 248
- 317
src/graph/graphMixins/ManipulationMixin.js View File

@ -16,203 +16,94 @@ var manipulationMixin = {
}, },
/**
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
* @private
*/
_createManipulatorBar : function() {
// remove bound functions
this.off('select', this.boundFunction);
_restoreOverloadedFunctions : function() {
for (var functionName in this.cachedFunctions) {
if (this.cachedFunctions.hasOwnProperty(functionName)) {
this[functionName] = this.cachedFunctions[functionName];
}
}
},
// reset global variables
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
_toggleEditMode : function() {
this.editMode = !this.editMode;
var toolbar = document.getElementById("graph-manipulationDiv")
if (this.editMode == true) {
toolbar.style.display="block";
} }
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI add' id='manipulate-addNode'><span class='manipulationLabel'>Add Node</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI edit' id='manipulate-editNode'><span class='manipulationLabel'>Edit Selected</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI connect' id='manipulate-connectNode'><span class='manipulationLabel'>Connect Node</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI delete' id='manipulate-delete'><span class='manipulationLabel'>Delete selected</span></span>";
// bind the icons
var addButton = document.getElementById("manipulate-addNode");
addButton.onclick = this._createAddToolbar.bind(this);
var editButton = document.getElementById("manipulate-editNode");
editButton.onclick = this._createEditToolbar.bind(this);
var connectButton = document.getElementById("manipulate-connectNode");
connectButton.onclick = this._createConnectToolbar.bind(this);
var deleteButton = document.getElementById("manipulate-delete");
deleteButton.onclick = this._createDeletionToolbar.bind(this);
else {
toolbar.style.display="none";
}
this._createManipulatorBar()
}, },
/** /**
* Create the toolbar for adding Nodes
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
* *
* @private * @private
*/ */
_createAddToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
_createManipulatorBar : function() {
// remove bound functions
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span class='manipulationLabel'>Click in an empty space to place a new node</span></span>";
// bind the icon
var backButton = document.getElementById("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.
this.boundFunction = this._addNode.bind(this);
this.on('select', this.boundFunction);
},
// restore overloaded functions
this._restoreOverloadedFunctions();
// resume calculation
this.freezeSimulation = false;
/**
* Create the toolbar to edit nodes or edges.
* TODO: edges not implemented yet, unsure what to edit.
*
* @private
*/
_createEditToolbar : function() {
// clear the toolbar
// reset global variables
this.blockConnectingEdgeSelection = false; this.blockConnectingEdgeSelection = false;
this._clearManipulatorBar();
this.off('select', this.boundFunction);
this.forceAppendSelection = false
var message = "";
if (this._selectionIsEmpty()) {
message = "Select a node or edge to edit.";
}
else {
if (this._getSelectedObjectCount() > 1) {
message = "Select a single node or edge to edit."
this._unselectAll(true);
if (this.editMode == true) {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
} }
else {
if (this._clusterInSelection()) {
message = "You cannot edit a cluster."
this._unselectAll(true);
}
else {
if (this._getSelectedNodeCount() > 0) { // the selected item is a node
this._createEditNodeToolbar();
}
else { // the selected item is an edge
this._createEditEdgeToolbar();
}
}
}
}
if (message != "") {
this.blockConnectingEdgeSelection = true;
// create the toolbar contents
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" + this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<span class='manipulationUI add' id='manipulate-addNode'><span class='manipulationLabel'>Add Node</span></span>" +
"<div class='seperatorLine'></div>" + "<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span class='manipulationLabel'>"+message+"</span></span>";
"<span class='manipulationUI connect' id='manipulate-connectNode'><span class='manipulationLabel'>Add Link</span></span>";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI edit' id='manipulate-editNode'><span class='manipulationLabel'>Edit Node</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI delete' id='manipulate-delete'><span class='manipulationLabel'>Delete selected</span></span>";
}
// bind the icon
var backButton = document.getElementById("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.
this.boundFunction = this._createEditToolbar.bind(this);
// bind the icons
var addNodeButton = document.getElementById("manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
var addEdgeButton = document.getElementById("manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
var editButton = document.getElementById("manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction); this.on('select', this.boundFunction);
} }
}, },
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
* TODO: change shape or group?
*
* @private
*/
_createEditNodeToolbar : function() {
// clear the toolbar
this.blockConnectingEdgeSelection = false;
this._clearManipulatorBar();
this.off('select', this.boundFunction);
var editObject = this._getEditObject();
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Cancel</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none'>label: <input type='text' class='manipulatorInput' value='" + editObject.label + "' id='manipulator-obj-label' /></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none'>color: <input type='text' class='manipulatorInput' value='" + editObject.color.background + "' id='manipulator-obj-color' /></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none'><input type='button' class='manipulatorInput' value='save' id='manipulator-obj-save' /></span>"
// bind the icon
var backButton = document.getElementById("manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
var saveButton = document.getElementById("manipulator-obj-save");
saveButton.onclick = this._saveNodeData.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
},
/**
* save the changes in the node data
*
* @private
*/
_saveNodeData : function() {
var editObjectId = this._getEditObject().id;
var label = document.getElementById('manipulator-obj-label').value;
var definedColor = document.getElementById('manipulator-obj-color').value;
var hsv = util.hexToHSV(definedColor);
var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
var updatedSettings = {id:editObjectId,
label: label,
color: {
background:definedColor,
border:darkerColorHex,
highlight: {
background:lighterColorHex,
border:darkerColorHex
}
}};
this.nodesData.update(updatedSettings);
this._createManipulatorBar();
},
/** /**
* creating the toolbar to edit edges.
* Create the toolbar for adding Nodes
* *
* @private * @private
*/ */
_createEditEdgeToolbar : function() {
_createAddNodeToolbar : function() {
// clear the toolbar // clear the toolbar
this.blockConnectingEdgeSelection = false;
this._clearManipulatorBar(); this._clearManipulatorBar();
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
@ -220,14 +111,14 @@ var manipulationMixin = {
this.manipulationDiv.innerHTML = "" + this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" + "<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" + "<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span class='manipulationLabel'>Currently only nodes can be edited.</span></span>";
"<span class='manipulationUI none' id='manipulate-back'><span class='manipulationLabel'>Click in an empty space to place a new node</span></span>";
// bind the icon // bind the icon
var backButton = document.getElementById("manipulate-back"); var backButton = document.getElementById("manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this); backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._createManipulatorBar.bind(this);
this.boundFunction = this._addNode.bind(this);
this.on('select', this.boundFunction); this.on('select', this.boundFunction);
}, },
@ -237,9 +128,12 @@ var manipulationMixin = {
* *
* @private * @private
*/ */
_createConnectToolbar : function() {
_createAddEdgeToolbar : function() {
// clear the toolbar // clear the toolbar
this._clearManipulatorBar(); this._clearManipulatorBar();
this._unselectAll(true);
this.freezeSimulation = true;
this.off('select', this.boundFunction); this.off('select', this.boundFunction);
this._unselectAll(); this._unselectAll();
@ -249,7 +143,7 @@ var manipulationMixin = {
this.manipulationDiv.innerHTML = "" + this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" + "<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" + "<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span id='manipulatorLabel' class='manipulationLabel'>Select the node you want to connect to other nodes.</span></span>";
"<span class='manipulationUI none' id='manipulate-back'><span id='manipulatorLabel' class='manipulationLabel'>Click on a node and drag the edge to another node.</span></span>";
// bind the icon // bind the icon
var backButton = document.getElementById("manipulate-back"); var backButton = document.getElementById("manipulate-back");
@ -258,43 +152,16 @@ var manipulationMixin = {
// we use the boundFunction so we can reference it when we unbind it from the "select" event. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._handleConnect.bind(this); this.boundFunction = this._handleConnect.bind(this);
this.on('select', this.boundFunction); this.on('select', this.boundFunction);
},
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this._handleTouch = this._handleConnect;
this._handleOnRelease = this._finishConnect;
/**
* create the toolbar for deleting selected objects. User has to be sure.
*
* @private
*/
_createDeletionToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
this.off('select', this.boundFunction);
// redraw to show the unselect
this._redraw();
if (this._selectionIsEmpty()) {
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI none notification' id='manipulate-back'><span id='manipulatorLabel' class='manipulationLabel'>Cannot delete an empty selection.</span></span>";
var graph = this;
window.setTimeout (function() {graph._createManipulatorBar()},1500);
}
else {
this.manipulationDiv.innerHTML = "" +
"<span class='manipulationUI back' id='manipulate-back'><span class='manipulationLabel'>Back</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI none' id='manipulate-back'><span id='manipulatorLabel' class='manipulationLabel'>Are you sure? This cannot be undone.</span></span>" +
"<div class='seperatorLine'></div>" +
"<span class='manipulationUI acceptDelete' id='manipulate-acceptDelete'><span class='manipulationLabel'>Yes.</span></span>";
// bind the buttons
var backButton = document.getElementById("manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
var acceptDeleteButton = document.getElementById("manipulate-acceptDelete");
acceptDeleteButton.onclick = this._deleteSelected.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
}
}, },
@ -304,162 +171,226 @@ var manipulationMixin = {
* *
* @private * @private
*/ */
_handleConnect : function() {
this.forceAppendSelection = false;
if (this._clusterInSelection()) {
this._unselectClusters(true);
if (!this._selectionIsEmpty()) {
this._setManipulationMessage("You cannot connect a node to a cluster.");
this.forceAppendSelection = true;
}
else {
this._setManipulationMessage("You cannot connect anything to a cluster.");
}
}
else if (!this._selectionIsEmpty()) {
if (this._getSelectedNodeCount() == 2) {
this._connectNodes();
this._restoreSourceNode();
this._setManipulationMessage("Click on another node you want to connect this node to or go back.");
}
else {
this._setManipulationMessage("Click on the node you want to connect this node.");
this._setSourceNode();
this.forceAppendSelection = true;
_handleConnect : function(pointer) {
if (this._getSelectedNodeCount() == 0) {
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._selectObject(node,false);
// create a node the temporary line can look at
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetNode'].x = node.x;
this.sectors['support']['nodes']['targetNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetViaNode'].x = node.x;
this.sectors['support']['nodes']['targetViaNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
// create a temporary edge
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
this.edges['connectionEdge'].from = node;
this.edges['connectionEdge'].connected = true;
this.edges['connectionEdge'].smooth = true;
this.edges['connectionEdge'].selected = true;
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.gesture.touches[0]);
this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
};
this.moving = true;
this.start();
}
} }
} }
else {
this._setManipulationMessage("Select the node you want to connect to other nodes.");
}
}, },
_finishConnect : function(pointer) {
if (this._getSelectedNodeCount() == 1) {
/**
* returns the object that is selected
*
* @returns {*}
* @private
*/
_getEditObject : function() {
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
return this.selectionObj[objectId];
}
}
return null;
},
// restore the drag function
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
delete this.cachedFunctions["_handleOnDrag"];
// remember the edge id
var connectFromId = this.edges['connectionEdge'].fromId;
/**
* stores the first selected node for the connecting process as the source node. This allows us to remember the direction
*
* @private
*/
_setSourceNode : function() {
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
this.manipulationSourceNode = this.selectionObj[objectId];
// remove the temporary nodes and edge
delete this.edges['connectionEdge']
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._createEdge(connectFromId,node.id);
this._createManipulatorBar();
} }
} }
this._unselectAll();
} }
}, },
/** /**
* gets the node the source connects to.
* Adds a node on the specified location
* *
* @returns {*}
* @private
* @param {Object} pointer
*/ */
_getTargetNode : function() {
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
if (this.manipulationSourceNode.id != this.selectionObj[objectId].id) {
return this.selectionObj[objectId];
}
_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",fixed:false};
if (this.triggerFunctions.add) {
if (this.triggerFunctions.add.length == 2) {
var me = this;
this.triggerFunctions.add(defaultData, function(finalizedData) {
me.createNodeOnClick = true;
me.nodesData.add(finalizedData);
me.createNodeOnClick = false;
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert("The function for add does not support two arguments (data,callback).");
this._createManipulatorBar();
this.moving = true;
this.start();
} }
} }
else {
console.log("didnt use funciton")
this.createNodeOnClick = true;
this.nodesData.add(defaultData);
this.createNodeOnClick = false;
this._createManipulatorBar();
this.moving = true;
this.start();
}
} }
return null;
},
/**
* restore the selection back to only the sourcenode
*
* @private
*/
_restoreSourceNode : function() {
this._unselectAll(true);
this._selectObject(this.manipulationSourceNode);
}, },
/** /**
* change the description message on the toolbar
* connect two nodes with a new edge.
* *
* @param message
* @private * @private
*/ */
_setManipulationMessage : function(message) {
var messageSpan = document.getElementById('manipulatorLabel');
messageSpan.innerHTML = message;
},
/**
* Adds a node on the specified location
*
* @param {Object} pointer
*/
_addNode : function() {
if (this._selectionIsEmpty()) {
var positionObject = this._pointerToPositionObject(this.pointerPosition);
this.createNodeOnClick = true;
this.nodesData.add({id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false});
this.createNodeOnClick = false;
this.moving = true;
this.start();
_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 {
alert("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.
* 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 * @private
*/ */
_connectNodes : function() {
var targetNode = this._getTargetNode();
var sourceNode = this.manipulationSourceNode;
this.edgesData.add({from:sourceNode.id, to:targetNode.id})
this.moving = true;
this.start();
_editNode : function() {
if (this.triggerFunctions.edit && this.editMode == true) {
var node = this._getSelectedNode();
var data = {id:node.id,
label: node.label,
group: node.group,
shape: node.shape,
color: {
background:node.color.background,
border:node.color.border,
highlight: {
background:node.color.highlight.background,
border:node.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 {
alert("The function for edit does not support two arguments (data, callback).")
}
}
else {
alert("No edit function has been bound to this button.")
}
}, },
/** /**
* delete everything in the selection * delete everything in the selection
* TODO : place the alert in the gui.
*
* *
* @private * @private
*/ */
_deleteSelected : function() { _deleteSelected : function() {
if (!this._clusterInSelection()) {
var selectedNodes = this.getSelectedNodes();
var selectedEdges = this.getSelectedEdges();
this._removeEdges(selectedEdges);
this._removeNodes(selectedNodes);
this.moving = true;
this.start();
}
else {
alert("Clusters cannot be deleted.")
if (!this._selectionIsEmpty() && this.editMode == true) {
if (!this._clusterInSelection()) {
var selectedNodes = this.getSelectedNodes();
var selectedEdges = this.getSelectedEdges();
if (this.triggerFunctions.delete) {
var me = this;
var data = {nodes: selectedNodes, edges: selectedEdges};
if (this.triggerFunctions.delete.length = 2) {
this.triggerFunctions.delete(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
me.moving = true;
me.start();
});
}
else {
alert("The function for edit does not support two arguments (data, callback).")
}
}
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this.moving = true;
this.start();
}
}
else {
alert("Clusters cannot be deleted.");
}
} }
} }
}; };

+ 15
- 0
src/graph/graphMixins/MixinLoader.js View File

@ -149,6 +149,13 @@ var graphMixinLoaders = {
if (this.manipulationDiv === undefined) { if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div'); this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'graph-manipulationDiv'; this.manipulationDiv.className = 'graph-manipulationDiv';
this.manipulationDiv.id = 'graph-manipulationDiv';
if (this.editMode == true) {
this.manipulationDiv.style.display = "block";
}
else {
this.manipulationDiv.style.display = "none";
}
this.containerElement.insertBefore(this.manipulationDiv, this.frame); this.containerElement.insertBefore(this.manipulationDiv, this.frame);
} }
// load the manipulation functions // load the manipulation functions
@ -157,6 +164,14 @@ var graphMixinLoaders = {
// create the manipulator toolbar // create the manipulator toolbar
this._createManipulatorBar(); this._createManipulatorBar();
} }
else {
if (this.manipulationDiv !== undefined) {
this._createManipulatorBar();
this.containerElement.removeChild(this.manipulationDiv);
this.manipulationDiv = undefined;
this._clearMixin(manipulationMixin);
}
}
}, },

+ 18
- 1
src/graph/graphMixins/SelectionMixin.js View File

@ -263,6 +263,23 @@ var SelectionMixin = {
return count; return count;
}, },
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNode : function() {
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
return this.selectionObj[objectId];
}
}
}
return null;
},
/** /**
* return the number of selected edges * return the number of selected edges
@ -488,7 +505,7 @@ var SelectionMixin = {
* *
* @private * @private
*/ */
_handleOnRelease : function() {
_handleOnRelease : function(pointer) {
this.xIncrement = 0; this.xIncrement = 0;
this.yIncrement = 0; this.yIncrement = 0;
this.zoomIncrement = 0; this.zoomIncrement = 0;

+ 4
- 2
src/graph/graphMixins/physics/repulsion.js View File

@ -23,7 +23,9 @@ var repulsionMixin = {
var b = 4/3; var b = 4/3;
// repulsing forces between nodes // repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance;
var nodeDistance = this.constants.physics.repulsion.nodeDistance;
var minimumDistance = nodeDistance;
// we loop from i over all but the last entree in the array // 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 // 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++) { for (i = 0; i < nodeIndices.length-1; i++) {
@ -36,7 +38,7 @@ var repulsionMixin = {
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (combinedClusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance; var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { if (distance < 2*minimumDistance) {
angle = Math.atan2(dy, dx); angle = Math.atan2(dy, dx);

+ 4
- 0
src/util.js View File

@ -826,3 +826,7 @@ util.hexToHSV = function hexToHSV(hex) {
return util.RGBToHSV(rgb.r,rgb.g,rgb.b); return util.RGBToHSV(rgb.r,rgb.g,rgb.b);
} }
util.isValidHex = function isValidHex(hex) {
var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
return isOk;
}

Loading…
Cancel
Save