Browse Source

Implemented dragging items

css_transitions
josdejong 10 years ago
parent
commit
2feb514a2a
22 changed files with 889 additions and 331 deletions
  1. +1
    -0
      Jakefile.js
  2. +0
    -1
      examples/graph/20_navigation.html
  3. +1
    -0
      examples/graph/index.html
  4. +1
    -0
      package.json
  5. +7
    -0
      src/graph/ClusterMixin.js
  6. +12
    -2
      src/graph/Edge.js
  7. +100
    -44
      src/graph/Graph.js
  8. +2
    -2
      src/graph/Node.js
  9. +260
    -127
      src/graph/SelectionMixin.js
  10. +1
    -2
      src/module/imports.js
  11. +11
    -2
      src/timeline/Controller.js
  12. +38
    -17
      src/timeline/Range.js
  13. +7
    -7
      src/timeline/Stack.js
  14. +24
    -75
      src/timeline/Timeline.js
  15. +17
    -0
      src/timeline/component/Component.js
  16. +181
    -0
      src/timeline/component/ItemSet.js
  17. +57
    -46
      src/timeline/component/RootPanel.js
  18. +11
    -2
      src/timeline/component/item/Item.js
  19. +1
    -1
      src/timeline/component/item/ItemBox.js
  20. +1
    -1
      src/timeline/component/item/ItemPoint.js
  21. +2
    -2
      src/timeline/component/item/ItemRange.js
  22. +154
    -0
      src/util.js

+ 1
- 0
Jakefile.js View File

@ -82,6 +82,7 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js', './src/graph/Popup.js',
'./src/graph/Groups.js', './src/graph/Groups.js',
'./src/graph/Images.js', './src/graph/Images.js',
'./src/graph/manipulationMixin.js',
'./src/graph/SectorsMixin.js', './src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js', './src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js', './src/graph/SelectionMixin.js',

+ 0
- 1
examples/graph/20_navigation.html View File

@ -31,7 +31,6 @@
div.table_description { div.table_description {
width:100px; width:100px;
} }
</style> </style>
<script type="text/javascript" src="../../dist/vis.js"></script> <script type="text/javascript" src="../../dist/vis.js"></script>

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

@ -32,6 +32,7 @@
<p><a href="18_fully_random_nodes_clustering.html">18_fully_random_nodes_clustering.html</a></p> <p><a href="18_fully_random_nodes_clustering.html">18_fully_random_nodes_clustering.html</a></p>
<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="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p> <p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div> </div>

+ 1
- 0
package.json View File

@ -33,6 +33,7 @@
"moment": "latest", "moment": "latest",
"hammerjs": "1.0.5", "hammerjs": "1.0.5",
"mousetrap": "latest", "mousetrap": "latest",
"emitter-component": "latest",
"node-watch": "latest" "node-watch": "latest"
} }
} }

+ 7
- 0
src/graph/ClusterMixin.js View File

@ -335,6 +335,10 @@ var ClusterMixin = {
// if child node has been added on smaller scale than current, kick out // if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) { if (childNode.formationScale < this.scale || force == true) {
// remove the selection, first remove the selection from the connected edges
this._unselectConnectedEdges(parentNode);
parentNode.unselect();
// put the child node back in the global nodes object // put the child node back in the global nodes object
this.nodes[containedNodeId] = childNode; this.nodes[containedNodeId] = childNode;
@ -383,6 +387,9 @@ var ClusterMixin = {
// recalculate the size of the node on the next time the node is rendered // recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache(); parentNode.clearSizeCache();
// this unselects the rest of the edges
this._unselectConnectedEdges(parentNode);
} }
// check if a further expansion step is possible if recursivity is enabled // check if a further expansion step is possible if recursivity is enabled

+ 12
- 2
src/graph/Edge.js View File

@ -32,6 +32,7 @@ function Edge (properties, graph, constants) {
this.width = constants.edges.width; this.width = constants.edges.width;
this.value = undefined; this.value = undefined;
this.length = constants.edges.length; this.length = constants.edges.length;
this.selected = false;
this.from = null; // a node this.from = null; // a node
this.to = null; // a node this.to = null; // a node
@ -268,7 +269,7 @@ Edge.prototype._drawLine = function(ctx) {
* @private * @private
*/ */
Edge.prototype._getLineWidth = function() { Edge.prototype._getLineWidth = function() {
if (this.from.selected || this.to.selected) {
if (this.selected == true) {
return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv; return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
} }
else { else {
@ -617,4 +618,13 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
*/ */
Edge.prototype.setScale = function(scale) { Edge.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale; this.graphScaleInv = 1.0/scale;
};
};
Edge.prototype.select = function() {
this.selected = true;
}
Edge.prototype.unselect = function() {
this.selected = false;
}

+ 100
- 44
src/graph/Graph.js View File

@ -94,6 +94,9 @@ function Graph (container, data, options) {
enabled: false, enabled: false,
speed: {x: 10, y: 10, zoom: 0.02} speed: {x: 10, y: 10, zoom: 0.02}
}, },
dataManipulationToolbar: {
enabled: false
},
minVelocity: 2, // px/s minVelocity: 2, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
}; };
@ -116,15 +119,15 @@ function Graph (container, data, options) {
// load the sector system. (mandatory, fully integrated with Graph) // load the sector system. (mandatory, fully integrated with Graph)
this._loadSectorSystem(); this._loadSectorSystem();
// apply options
this.setOptions(options);
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
this._loadClusterSystem(); this._loadClusterSystem();
// load the selection system. (mandatory, required by Graph) // load the selection system. (mandatory, required by Graph)
this._loadSelectionSystem(); this._loadSelectionSystem();
// apply options
this.setOptions(options);
// other vars // other vars
var graph = this; var graph = this;
this.freezeSimulation = false;// freeze the simulation this.freezeSimulation = false;// freeze the simulation
@ -135,6 +138,7 @@ function Graph (container, data, options) {
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. 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.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.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.scale = 1; // defining the global scale variable in the constructor
@ -417,6 +421,17 @@ Graph.prototype.setOptions = function (options) {
this.constants.keyboard.enabled = false; this.constants.keyboard.enabled = false;
} }
if (options.dataManipulationToolbar) {
this.constants.dataManipulationToolbar.enabled = true;
for (var prop in options.dataManipulationToolbar) {
if (options.dataManipulationToolbar.hasOwnProperty(prop)) {
this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop];
}
}
}
else if (options.dataManipulationToolbar !== undefined) {
this.constants.dataManipulationToolbar.enabled = false;
}
// TODO: work out these options and document them // TODO: work out these options and document them
if (options.edges) { if (options.edges) {
@ -478,16 +493,18 @@ Graph.prototype.setOptions = function (options) {
} }
} }
this.setSize(this.width, this.height);
this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
this._setScale(1);
// load the navigation system. // load the navigation system.
this._loadNavigationControls(); this._loadNavigationControls();
// load the data manipulation system
this._loadManipulationSystem();
// bind keys. If disabled, this will not do anything; // bind keys. If disabled, this will not do anything;
this._createKeyBinds(); this._createKeyBinds();
this.setSize(this.width, this.height);
this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
this._setScale(1);
this._redraw(); this._redraw();
}; };
@ -546,6 +563,7 @@ Graph.prototype._create = function () {
this.frame.className = 'graph-frame'; this.frame.className = 'graph-frame';
this.frame.style.position = 'relative'; this.frame.style.position = 'relative';
this.frame.style.overflow = 'hidden'; this.frame.style.overflow = 'hidden';
this.frame.style.zIndex = "1";
// create the graph canvas (HTML canvas element) // create the graph canvas (HTML canvas element)
this.frame.canvas = document.createElement( 'canvas' ); this.frame.canvas = document.createElement( 'canvas' );
@ -581,6 +599,7 @@ Graph.prototype._create = function () {
// add the frame to the container element // add the frame to the container element
this.containerElement.appendChild(this.frame); this.containerElement.appendChild(this.frame);
}; };
@ -616,14 +635,10 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
} }
/*
this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mousetrap.bind("-",this.increaseClusterLevel.bind(me));
this.mousetrap.bind("s",this.singleStep.bind(me));
this.mousetrap.bind("h",this.updateClustersDefault.bind(me));
this.mousetrap.bind("c",this._collapseSector.bind(me));
this.mousetrap.bind("f",this.toggleFreeze.bind(me));
*/
if (this.constants.dataManipulationToolbar.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
}
} }
/** /**
@ -670,31 +685,32 @@ Graph.prototype._onDragStart = function () {
drag.nodeId = node.id; drag.nodeId = node.id;
// select the clicked node if not yet selected // select the clicked node if not yet selected
if (!node.isSelected()) { if (!node.isSelected()) {
this._selectNode(node,false);
this._selectObject(node,false);
} }
// create an array with the selected nodes and their original location and status // create an array with the selected nodes and their original location and status
var me = this;
this.selection.forEach(function (id) {
var node = me.nodes[id];
if (node) {
var s = {
id: id,
node: node,
// store original x, y, xFixed and yFixed, make the node temporarily Fixed
x: node.x,
y: node.y,
xFixed: node.xFixed,
yFixed: node.yFixed
};
node.xFixed = true;
node.yFixed = true;
drag.selection.push(s);
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
var object = this.selectionObj[objectId];
if (object instanceof Node) {
var s = {
id: object.id,
node: object,
// store original x, y, xFixed and yFixed, make the node temporarily Fixed
x: object.x,
y: object.y,
xFixed: object.xFixed,
yFixed: object.yFixed
};
object.xFixed = true;
object.yFixed = true;
drag.selection.push(s);
}
} }
});
}
} }
}; };
@ -771,7 +787,9 @@ Graph.prototype._onDragEnd = function () {
*/ */
Graph.prototype._onTap = function (event) { Graph.prototype._onTap = function (event) {
var pointer = this._getPointer(event.gesture.touches[0]); var pointer = this._getPointer(event.gesture.touches[0]);
this.pointerPosition = pointer;
this._handleTap(pointer); this._handleTap(pointer);
}; };
@ -792,6 +810,7 @@ Graph.prototype._onDoubleTap = function (event) {
*/ */
Graph.prototype._onHold = function (event) { Graph.prototype._onHold = function (event) {
var pointer = this._getPointer(event.gesture.touches[0]); var pointer = this._getPointer(event.gesture.touches[0]);
this.pointerPosition = pointer;
this._handleOnHold(pointer); this._handleOnHold(pointer);
}; };
@ -1120,6 +1139,10 @@ Graph.prototype.setSize = function(width, height) {
this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.width = this.frame.canvas.clientWidth;
this.frame.canvas.height = this.frame.canvas.clientHeight; this.frame.canvas.height = this.frame.canvas.clientHeight;
if (this.manipulationDiv !== undefined) {
this.manipulationDiv.style.width = this.frame.canvas.clientWidth;
}
if (this.constants.navigation.enabled == true) { if (this.constants.navigation.enabled == true) {
this._relocateNavigation(); this._relocateNavigation();
} }
@ -1184,7 +1207,7 @@ Graph.prototype._addNodes = function(ids) {
var node = new Node(data, this.images, this.groups, this.constants); var node = new Node(data, this.images, this.groups, this.constants);
this.nodes[id] = node; // note: this may replace an existing node this.nodes[id] = node; // note: this may replace an existing node
if (!node.isFixed()) {
if (!node.isFixed() && this.createNodeOnClick != true) {
// TODO: position new nodes in a smarter way! // TODO: position new nodes in a smarter way!
var radius = this.constants.edges.length * 2; var radius = this.constants.edges.length * 2;
var count = ids.length; var count = ids.length;
@ -1200,6 +1223,7 @@ Graph.prototype._addNodes = function(ids) {
this._updateNodeIndexList(); this._updateNodeIndexList();
this._reconnectEdges(); this._reconnectEdges();
this._updateValueRange(this.nodes); this._updateValueRange(this.nodes);
this.updateLabels();
}; };
/** /**
@ -1468,7 +1492,7 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawAllSectorNodes",ctx); this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx); this._doInAllSectors("_drawEdges",ctx);
this._doInAllSectors("_drawNodes",ctx);
this._doInAllSectors("_drawNodes",ctx,true);
// restore original scaling and translation // restore original scaling and translation
ctx.restore(); ctx.restore();
@ -1722,6 +1746,7 @@ Graph.prototype._calculateForces = function() {
// 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
var a_base = (-2/3); var b = 4/3;
for (i = 0; i < this.nodeIndices.length-1; i++) { for (i = 0; i < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]]; node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) { for (j = i+1; j < this.nodeIndices.length; j++) {
@ -1734,6 +1759,7 @@ Graph.prototype._calculateForces = function() {
// clusters have a larger region of influence // clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx); angle = Math.atan2(dy, dx);
@ -1744,13 +1770,13 @@ Graph.prototype._calculateForces = function() {
// TODO: correct factor for repulsing force // TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
//repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
repulsingForce = a * distance + b; // TODO: test the approximation of the function above
} }
// amplify the repulsion for clusters. // amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
repulsingForce *= this.forceFactor; repulsingForce *= this.forceFactor;
fx = Math.cos(angle) * repulsingForce; fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce ; fy = Math.sin(angle) * repulsingForce ;
@ -1963,9 +1989,9 @@ Graph.prototype.start = function() {
} }
}; };
/**
* Debug function, does one step of the graph
*/
Graph.prototype.singleStep = function() { Graph.prototype.singleStep = function() {
if (this.moving) { if (this.moving) {
this._initializeForceCalculation(); this._initializeForceCalculation();
@ -2044,7 +2070,6 @@ Graph.prototype._loadSectorSystem = function() {
* @private * @private
*/ */
Graph.prototype._loadSelectionSystem = function() { Graph.prototype._loadSelectionSystem = function() {
this.selection = [];
this.selectionObj = {}; this.selectionObj = {};
for (var mixinFunction in SelectionMixin) { for (var mixinFunction in SelectionMixin) {
@ -2055,6 +2080,37 @@ Graph.prototype._loadSelectionSystem = function() {
} }
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadManipulationSystem = function() {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
if (this.constants.dataManipulationToolbar.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'graph-manipulationDiv';
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
// load the manipulation functions
for (var mixinFunction in manipulationMixin) {
if (manipulationMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = manipulationMixin[mixinFunction];
}
}
// create the manipulator toolbar
this._createManipulatorBar();
}
}
/** /**
* Mixin the navigation (User Interface) system and initialize the parameters required * Mixin the navigation (User Interface) system and initialize the parameters required
* *

+ 2
- 2
src/graph/Node.js View File

@ -188,8 +188,8 @@ Node.prototype.setProperties = function(properties, constants) {
} }
} }
this.xFixed = this.xFixed || (properties.x !== undefined);
this.yFixed = this.yFixed || (properties.y !== undefined);
this.xFixed = this.xFixed || (properties.x !== undefined && properties.fixed);
this.yFixed = this.yFixed || (properties.y !== undefined && properties.fixed);
this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
if (this.shape == 'image') { if (this.shape == 'image') {

+ 260
- 127
src/graph/SelectionMixin.js View File

@ -108,7 +108,7 @@ var SelectionMixin = {
_getNodeAt : function (pointer) { _getNodeAt : function (pointer) {
// we first check if this is an navigation controls element // we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer); var positionObject = this._pointerToPositionObject(pointer);
overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the // if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others // one which is drawn on top of the others
@ -121,6 +121,36 @@ var SelectionMixin = {
}, },
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getEdgesOverlappingWith : function (object, overlappingEdges) {
var edges = this.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
},
/**
* retrieve all nodes 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
*/
_getAllEdgesOverlappingWith : function (object) {
var overlappingEdges = [];
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
return overlappingEdges;
},
/** /**
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
@ -130,18 +160,25 @@ var SelectionMixin = {
* @private * @private
*/ */
_getEdgeAt : function(pointer) { _getEdgeAt : function(pointer) {
return null;
var positionObject = this._pointerToPositionObject(pointer);
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
}
}, },
/** /**
* Add object to the selection array. The this.selection id array may not be needed.
* Add object to the selection array.
* *
* @param obj * @param obj
* @private * @private
*/ */
_addToSelection : function(obj) { _addToSelection : function(obj) {
this.selection.push(obj.id);
this.selectionObj[obj.id] = obj; this.selectionObj[obj.id] = obj;
}, },
@ -149,16 +186,10 @@ var SelectionMixin = {
/** /**
* Remove a single option from selection. * Remove a single option from selection.
* *
* @param obj
* @param {Object} obj
* @private * @private
*/ */
_removeFromSelection : function(obj) { _removeFromSelection : function(obj) {
for (var i = 0; i < this.selection.length; i++) {
if (obj.id == this.selection[i]) {
this.selection.splice(i,1);
break;
}
}
delete this.selectionObj[obj.id]; delete this.selectionObj[obj.id];
}, },
@ -174,10 +205,9 @@ var SelectionMixin = {
doNotTrigger = false; doNotTrigger = false;
} }
this.selection = [];
for (var objId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objId)) {
this.selectionObj[objId].unselect();
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
this.selectionObj[objectId].unselect();
} }
} }
this.selectionObj = {}; this.selectionObj = {};
@ -189,6 +219,89 @@ var SelectionMixin = {
} }
}, },
/**
* Unselect all clusters. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_unselectClusters : function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
if (this.selectionObj[objectId].clusterSize > 1) {
this.selectionObj[objectId].unselect();
this._removeFromSelection(this.selectionObj[objectId]);
}
}
}
}
if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
}
},
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNodeCount : function() {
var count = 0;
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
count += 1;
}
}
}
return count;
},
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
_getSelectedEdgeCount : function() {
var count = 0;
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Edge) {
count += 1;
}
}
}
return count;
},
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
_getSelectedObjectCount : function() {
var count = 0;
for (var objectId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objectId)) {
count += 1;
}
}
return count;
},
/** /**
* Check if anything is selected * Check if anything is selected
@ -197,41 +310,93 @@ var SelectionMixin = {
* @private * @private
*/ */
_selectionIsEmpty : function() { _selectionIsEmpty : function() {
if (this.selection.length == 0) {
return true;
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
return false;
}
} }
else {
return false;
return true;
},
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
_clusterInSelection : function() {
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
if (this.selectionObj[objectId].clusterSize > 1) {
return true;
}
}
}
}
return false;
},
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_selectConnectedEdges : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.select();
this._addToSelection(edge);
}
},
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_unselectConnectedEdges : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.unselect();
this._removeFromSelection(edge);
} }
}, },
/** /**
* This is called when someone clicks on a node. either select or deselect it. * This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection * If there is an existing selection and we don't want to append to it, clear the existing selection
* *
* @param {Node} node
* @param {Node || Edge} object
* @param {Boolean} append * @param {Boolean} append
* @param {Boolean} [doNotTrigger] | ignore trigger * @param {Boolean} [doNotTrigger] | ignore trigger
* @private * @private
*/ */
_selectNode : function(node, append, doNotTrigger) {
_selectObject : function(object, append, doNotTrigger) {
if (doNotTrigger === undefined) { if (doNotTrigger === undefined) {
doNotTrigger = false; doNotTrigger = false;
} }
if (this._selectionIsEmpty() == false && append == false) {
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true); this._unselectAll(true);
} }
if (node.selected == false) {
node.select();
this._addToSelection(node);
if (object.selected == false) {
object.select();
this._addToSelection(object);
if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
this._selectConnectedEdges(object);
}
} }
else { else {
node.unselect();
this._removeFromSelection(node);
object.unselect();
this._removeFromSelection(object);
} }
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select', { this._trigger('select', {
@ -251,6 +416,7 @@ var SelectionMixin = {
*/ */
_handleTouch : function(pointer) { _handleTouch : function(pointer) {
if (this.constants.navigation.enabled == true) { if (this.constants.navigation.enabled == true) {
this.pointerPosition = pointer;
var node = this._getNavigationNodeAt(pointer); var node = this._getNavigationNodeAt(pointer);
if (node != null) { if (node != null) {
if (this[node.triggerFunction] !== undefined) { if (this[node.triggerFunction] !== undefined) {
@ -270,10 +436,16 @@ var SelectionMixin = {
_handleTap : function(pointer) { _handleTap : function(pointer) {
var node = this._getNodeAt(pointer); var node = this._getNodeAt(pointer);
if (node != null) { if (node != null) {
this._selectNode(node,false);
this._selectObject(node,false);
} }
else { else {
this._unselectAll();
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,false);
}
else {
this._unselectAll();
}
} }
this._redraw(); this._redraw();
}, },
@ -305,7 +477,13 @@ var SelectionMixin = {
_handleOnHold : function(pointer) { _handleOnHold : function(pointer) {
var node = this._getNodeAt(pointer); var node = this._getNodeAt(pointer);
if (node != null) { if (node != null) {
this._selectNode(node,true);
this._selectObject(node,true);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,true);
}
} }
this._redraw(); this._redraw();
}, },
@ -327,27 +505,54 @@ var SelectionMixin = {
/** /**
* *
* retrieve the currently selected nodes
* retrieve the currently selected objects
* @return {Number[] | String[]} selection An array with the ids of the * @return {Number[] | String[]} selection An array with the ids of the
* selected nodes. * selected nodes.
*/ */
getSelection : function() { getSelection : function() {
return this.selection.concat([]);
var nodeIds = this.getSelectedNodes();
var edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
}, },
/** /**
* *
* retrieve the currently selected nodes as objects
* @return {Objects} selection An array with the ids of the
* retrieve the currently selected nodes
* @return {String} selection An array with the ids of the
* selected nodes. * selected nodes.
*/ */
getSelectionObjects : function() {
return this.selectionObj;
getSelectedNodes : function() {
var idArray = [];
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
idArray.push(objectId);
}
}
}
return idArray
}, },
/** /**
* // TODO: rework this function, it is from the old system
* *
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
getSelectedEdges : function() {
var idArray = [];
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Edge) {
idArray.push(objectId);
}
}
}
return idArray
},
/**
* select zero or more nodes * select zero or more nodes
* @param {Number[] | String[]} selection An array with the ids of the * @param {Number[] | String[]} selection An array with the ids of the
* selected nodes. * selected nodes.
@ -368,89 +573,34 @@ var SelectionMixin = {
if (!node) { if (!node) {
throw new RangeError('Node with id "' + id + '" not found'); throw new RangeError('Node with id "' + id + '" not found');
} }
this._selectNode(node,true,true);
this._selectObject(node,true,true);
} }
this.redraw(); this.redraw();
}, },
/** /**
* TODO: rework this function, it is from the old system
*
* Validate the selection: remove ids of nodes which no longer exist * Validate the selection: remove ids of nodes which no longer exist
* @private * @private
*/ */
_updateSelection : function () { _updateSelection : function () {
var i = 0;
while (i < this.selection.length) {
var nodeId = this.selection[i];
if (!this.nodes.hasOwnProperty(nodeId)) {
this.selection.splice(i, 1);
delete this.selectionObj[nodeId];
}
else {
i++;
}
}
}
/**
* Unselect selected nodes. If no selection array is provided, all nodes
* are unselected
* @param {Object[]} selection Array with selection objects, each selection
* object has a parameter row. Optional
* @param {Boolean} triggerSelect If true (default), the select event
* is triggered when nodes are unselected
* @return {Boolean} changed True if the selection is changed
* @private
*/
/* _unselectNodes : function(selection, triggerSelect) {
var changed = false;
var i, iMax, id;
if (selection) {
// remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0;
while (j < this.selection.length) {
if (this.selection[j] == id) {
this.selection.splice(j, 1);
changed = true;
}
else {
j++;
for(var objectId in this.selectionObj) {
if(this.selectionObj.hasOwnProperty(objectId)) {
if (this.selectionObj[objectId] instanceof Node) {
if (!this.nodes.hasOwnProperty(objectId)) {
delete this.selectionObj[objectId];
} }
} }
}
}
else if (this.selection && this.selection.length) {
// remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i];
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
else { // assuming only edges and nodes are selected
if (!this.edges.hasOwnProperty(objectId)) {
delete this.selectionObj[objectId];
}
} }
changed = true;
} }
this.selection = [];
}
if (changed && (triggerSelect == true || triggerSelect == undefined)) {
// fire the select event
this._trigger('select', {
nodes: this.getSelection()
});
} }
}
return changed;
},
*/
}
/** /**
* select all nodes on given location x, y * select all nodes on given location x, y
* @param {Array} selection an array with node ids * @param {Array} selection an array with node ids
@ -475,40 +625,23 @@ var SelectionMixin = {
if (selection[i] != this.selection[i]) { if (selection[i] != this.selection[i]) {
selectionAlreadyThere = false; selectionAlreadyThere = false;
break; break;
>>>>>>> develop
} }
} }
} }
if (selectionAlreadyThere) {
return changed;
}
if (append == undefined || append == false) {
// first deselect any selected node
var triggerSelect = false;
changed = this._unselectNodes(undefined, triggerSelect);
}
}
for (i = 0, iMax = selection.length; i < iMax; i++) {
// add each of the new selections, but only when they are not duplicate
var id = selection[i];
var isDuplicate = (this.selection.indexOf(id) != -1);
if (!isDuplicate) {
this.nodes[id].select();
this.selection.push(id);
changed = true;
}
}
<<<<<<< HEAD
=======
if (changed) { if (changed) {
// fire the select event // fire the select event
this._trigger('select', { this._trigger('select', {
nodes: this.getSelection() nodes: this.getSelection()
}); });
} }
>>>>>>> develop
return changed;
},
*/
}; };

+ 1
- 2
src/module/imports.js View File

@ -6,6 +6,7 @@
// If not available there, load via require. // If not available there, load via require.
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
var Emitter = require('emitter-component');
var Hammer; var Hammer;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@ -28,5 +29,3 @@ else {
throw Error('mouseTrap is only available in a browser, not in node.js.'); throw Error('mouseTrap is only available in a browser, not in node.js.');
} }
} }

+ 11
- 2
src/timeline/Controller.js View File

@ -11,6 +11,9 @@ function Controller () {
this.reflowTimer = undefined; this.reflowTimer = undefined;
} }
// Extend controller with Emitter mixin
Emitter(Controller.prototype);
/** /**
* Add a component to the controller * Add a component to the controller
* @param {Component} component * @param {Component} component
@ -26,7 +29,7 @@ Controller.prototype.add = function add(component) {
} }
// add the component // add the component
component.controller = this;
component.setController(this);
this.components[component.id] = component; this.components[component.id] = component;
}; };
@ -38,13 +41,17 @@ Controller.prototype.remove = function remove(component) {
var id; var id;
for (id in this.components) { for (id in this.components) {
if (this.components.hasOwnProperty(id)) { if (this.components.hasOwnProperty(id)) {
if (id == component || this.components[id] == component) {
if (id == component || this.components[id] === component) {
break; break;
} }
} }
} }
if (id) { if (id) {
// unregister the controller (gives the component the ability to unregister
// event listeners and clean up other stuff)
this.components[id].setController(null);
delete this.components[id]; delete this.components[id];
} }
}; };
@ -54,6 +61,7 @@ Controller.prototype.remove = function remove(component) {
* @param {Boolean} [force] If true, an immediate reflow is forced. Default * @param {Boolean} [force] If true, an immediate reflow is forced. Default
* is false. * is false.
*/ */
// TODO: change requestReflow into an event
Controller.prototype.requestReflow = function requestReflow(force) { Controller.prototype.requestReflow = function requestReflow(force) {
if (force) { if (force) {
this.reflow(); this.reflow();
@ -74,6 +82,7 @@ Controller.prototype.requestReflow = function requestReflow(force) {
* @param {Boolean} [force] If true, an immediate repaint is forced. Default * @param {Boolean} [force] If true, an immediate repaint is forced. Default
* is false. * is false.
*/ */
// TODO: change requestReflow into an event
Controller.prototype.requestRepaint = function requestRepaint(force) { Controller.prototype.requestRepaint = function requestRepaint(force) {
if (force) { if (force) {
this.repaint(); this.repaint();

+ 38
- 17
src/timeline/Range.js View File

@ -48,42 +48,48 @@ function validateDirection (direction) {
/** /**
* Add listeners for mouse and touch events to the component * Add listeners for mouse and touch events to the component
* @param {Component} component
* @param {Controller} controller
* @param {Component} component Should be a rootpanel
* @param {String} event Available events: 'move', 'zoom' * @param {String} event Available events: 'move', 'zoom'
* @param {String} direction Available directions: 'horizontal', 'vertical' * @param {String} direction Available directions: 'horizontal', 'vertical'
*/ */
Range.prototype.subscribe = function (component, event, direction) {
Range.prototype.subscribe = function (controller, component, event, direction) {
var me = this; var me = this;
if (event == 'move') { if (event == 'move') {
// drag start listener // drag start listener
component.on('dragstart', function (event) {
controller.on('dragstart', function (event) {
me._onDragStart(event, component); me._onDragStart(event, component);
}); });
// drag listener // drag listener
component.on('drag', function (event) {
controller.on('drag', function (event) {
me._onDrag(event, component, direction); me._onDrag(event, component, direction);
}); });
// drag end listener // drag end listener
component.on('dragend', function (event) {
controller.on('dragend', function (event) {
me._onDragEnd(event, component); me._onDragEnd(event, component);
}); });
// ignore dragging when holding
controller.on('hold', function (event) {
me._onHold();
});
} }
else if (event == 'zoom') { else if (event == 'zoom') {
// mouse wheel // mouse wheel
function mousewheel (event) { function mousewheel (event) {
me._onMouseWheel(event, component, direction); me._onMouseWheel(event, component, direction);
} }
component.on('mousewheel', mousewheel);
component.on('DOMMouseScroll', mousewheel); // For FF
controller.on('mousewheel', mousewheel);
controller.on('DOMMouseScroll', mousewheel); // For FF
// pinch // pinch
component.on('touch', function (event) {
me._onTouch();
controller.on('touch', function (event) {
me._onTouch(event);
}); });
component.on('pinch', function (event) {
controller.on('pinch', function (event) {
me._onPinch(event, component, direction); me._onPinch(event, component, direction);
}); });
} }
@ -311,7 +317,7 @@ var touchParams = {};
Range.prototype._onDragStart = function(event, component) { Range.prototype._onDragStart = function(event, component) {
// refuse to drag when we where pinching to prevent the timeline make a jump // refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen // when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
touchParams.start = this.start; touchParams.start = this.start;
touchParams.end = this.end; touchParams.end = this.end;
@ -334,7 +340,7 @@ Range.prototype._onDrag = function (event, component, direction) {
// refuse to drag when we where pinching to prevent the timeline make a jump // refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen // when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start), interval = (touchParams.end - touchParams.start),
@ -356,7 +362,7 @@ Range.prototype._onDrag = function (event, component, direction) {
Range.prototype._onDragEnd = function (event, component) { Range.prototype._onDragEnd = function (event, component) {
// refuse to drag when we where pinching to prevent the timeline make a jump // refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen // when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
if (component.frame) { if (component.frame) {
component.frame.style.cursor = 'auto'; component.frame.style.cursor = 'auto';
@ -417,14 +423,29 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
}; };
/** /**
* On start of a touch gesture, initialize scale to 1
* Start of a touch gesture
* @private * @private
*/ */
Range.prototype._onTouch = function () {
Range.prototype._onTouch = function (event) {
touchParams.start = this.start; touchParams.start = this.start;
touchParams.end = this.end; touchParams.end = this.end;
touchParams.pinching = false;
touchParams.ignore = false;
touchParams.center = null; touchParams.center = null;
// don't move the range when dragging a selected event
// TODO: it's not so neat to have to know about the state of the ItemSet
var item = ItemSet.itemFromTarget(event);
if (item && item.selected) {
touchParams.ignore = true;
}
};
/**
* On start of a hold gesture
* @private
*/
Range.prototype._onHold = function () {
touchParams.ignore = true;
}; };
/** /**
@ -435,7 +456,7 @@ Range.prototype._onTouch = function () {
* @private * @private
*/ */
Range.prototype._onPinch = function (event, component, direction) { Range.prototype._onPinch = function (event, component, direction) {
touchParams.pinching = true;
touchParams.ignore = true;
if (event.gesture.touches.length > 1) { if (event.gesture.touches.length > 1) {
if (!touchParams.center) { if (!touchParams.center) {

+ 7
- 7
src/timeline/Stack.js View File

@ -1,11 +1,11 @@
/** /**
* @constructor Stack * @constructor Stack
* Stacks items on top of each other. * Stacks items on top of each other.
* @param {ItemSet} parent
* @param {ItemSet} itemset
* @param {Object} [options] * @param {Object} [options]
*/ */
function Stack (parent, options) {
this.parent = parent;
function Stack (itemset, options) {
this.itemset = itemset;
this.options = options || {}; this.options = options || {};
this.defaultOptions = { this.defaultOptions = {
@ -43,14 +43,14 @@ function Stack (parent, options) {
/** /**
* Set options for the stack * Set options for the stack
* @param {Object} options Available options: * @param {Object} options Available options:
* {ItemSet} parent
* {ItemSet} itemset
* {Number} margin * {Number} margin
* {function} order Stacking order * {function} order Stacking order
*/ */
Stack.prototype.setOptions = function setOptions (options) { Stack.prototype.setOptions = function setOptions (options) {
util.extend(this.options, options); util.extend(this.options, options);
// TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
// TODO: register on data changes at the connected itemset, and update the changed part only and immediately
}; };
/** /**
@ -70,9 +70,9 @@ Stack.prototype.update = function update() {
* @private * @private
*/ */
Stack.prototype._order = function _order () { Stack.prototype._order = function _order () {
var items = this.parent.items;
var items = this.itemset.items;
if (!items) { if (!items) {
throw new Error('Cannot stack items: parent does not contain items');
throw new Error('Cannot stack items: ItemSet does not contain items');
} }
// TODO: store the sorted items, to have less work later on // TODO: store the sorted items, to have less work later on

+ 24
- 75
src/timeline/Timeline.js View File

@ -1,7 +1,7 @@
/** /**
* Create a timeline visualization * Create a timeline visualization
* @param {HTMLElement} container * @param {HTMLElement} container
* @param {vis.DataSet | Array | DataTable} [items]
* @param {vis.DataSet | Array | google.visualization.DataTable} [items]
* @param {Object} [options] See Timeline.setOptions for the available options. * @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor * @constructor
*/ */
@ -45,6 +45,13 @@ function Timeline (container, items, options) {
this.rootPanel = new RootPanel(container, rootOptions); this.rootPanel = new RootPanel(container, rootOptions);
this.controller.add(this.rootPanel); this.controller.add(this.rootPanel);
// single select (or unselect) when tapping an item
// TODO: implement ctrl+click
this.controller.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.controller.on('hold', this._onMultiSelectItem.bind(this));
// item panel // item panel
var itemOptions = Object.create(this.options); var itemOptions = Object.create(this.options);
itemOptions.left = function () { itemOptions.left = function () {
@ -84,26 +91,20 @@ function Timeline (container, items, options) {
// TODO: reckon with options moveable and zoomable // TODO: reckon with options moveable and zoomable
// TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
this.range.subscribe(this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
// TODO: enable moving again
this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) { this.range.on('rangechange', function (properties) {
var force = true; var force = true;
me.controller.requestReflow(force); me.controller.requestReflow(force);
me._trigger('rangechange', properties);
me.emit('rangechange', properties);
}); });
this.range.on('rangechanged', function (properties) { this.range.on('rangechanged', function (properties) {
var force = true; var force = true;
me.controller.requestReflow(force); me.controller.requestReflow(force);
me._trigger('rangechanged', properties);
me.emit('rangechanged', properties);
}); });
// single select (or unselect) when tapping an item
// TODO: implement ctrl+click
this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// time axis // time axis
var timeaxisOptions = Object.create(rootOptions); var timeaxisOptions = Object.create(rootOptions);
timeaxisOptions.range = this.range; timeaxisOptions.range = this.range;
@ -140,6 +141,9 @@ function Timeline (container, items, options) {
} }
} }
// extend Timeline with the Emitter mixin
Emitter(Timeline.prototype);
/** /**
* Set options * Set options
* @param {Object} options TODO: describe the available options * @param {Object} options TODO: describe the available options
@ -173,7 +177,7 @@ Timeline.prototype.getCustomTime = function() {
/** /**
* Set items * Set items
* @param {vis.DataSet | Array | DataTable | null} items
* @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/ */
Timeline.prototype.setItems = function(items) { Timeline.prototype.setItems = function(items) {
var initialLoad = (this.itemsData == null); var initialLoad = (this.itemsData == null);
@ -234,7 +238,7 @@ Timeline.prototype.setItems = function(items) {
/** /**
* Set groups * Set groups
* @param {vis.DataSet | Array | DataTable} groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/ */
Timeline.prototype.setGroups = function(groups) { Timeline.prototype.setGroups = function(groups) {
var me = this; var me = this;
@ -367,56 +371,19 @@ Timeline.prototype.getSelection = function getSelection() {
return this.content ? this.content.getSelection() : []; return this.content ? this.content.getSelection() : [];
}; };
/**
* Add event listener
* @param {String} event Event name. Available events:
* 'rangechange', 'rangechanged', 'select'
* @param {function} callback Callback function, invoked as callback(properties)
* where properties is an optional object containing
* event specific properties.
*/
Timeline.prototype.on = function on (event, callback) {
var available = ['rangechange', 'rangechanged', 'select'];
if (available.indexOf(event) == -1) {
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
}
events.addListener(this, event, callback);
};
/**
* Remove an event listener
* @param {String} event Event name
* @param {function} callback Callback function
*/
Timeline.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
};
/**
* Trigger an event
* @param {String} event Event name, available events: 'rangechange',
* 'rangechanged', 'select'
* @param {Object} [properties] Event specific properties
* @private
*/
Timeline.prototype._trigger = function _trigger(event, properties) {
events.trigger(this, event, properties || {});
};
/** /**
* Handle selecting/deselecting an item when tapping it * Handle selecting/deselecting an item when tapping it
* @param {Event} event * @param {Event} event
* @private * @private
*/ */
// TODO: move this function to ItemSet
Timeline.prototype._onSelectItem = function (event) { Timeline.prototype._onSelectItem = function (event) {
var item = this._itemFromTarget(event);
var item = ItemSet.itemFromTarget(event);
var selection = item ? [item.id] : []; var selection = item ? [item.id] : [];
this.setSelection(selection); this.setSelection(selection);
this._trigger('select', {
this.emit('select', {
items: this.getSelection() items: this.getSelection()
}); });
@ -428,9 +395,10 @@ Timeline.prototype._onSelectItem = function (event) {
* @param {Event} event * @param {Event} event
* @private * @private
*/ */
// TODO: move this function to ItemSet
Timeline.prototype._onMultiSelectItem = function (event) { Timeline.prototype._onMultiSelectItem = function (event) {
var selection, var selection,
item = this._itemFromTarget(event);
item = ItemSet.itemFromTarget(event);
if (!item) { if (!item) {
// do nothing... // do nothing...
@ -449,28 +417,9 @@ Timeline.prototype._onMultiSelectItem = function (event) {
} }
this.setSelection(selection); this.setSelection(selection);
this._trigger('select', {
this.emit('select', {
items: this.getSelection() items: this.getSelection()
}); });
event.stopPropagation(); event.stopPropagation();
}; };
/**
* Find an item from an event target:
* searches for the attribute 'timeline-item' in the event target's element tree
* @param {Event} event
* @return {Item | null| item
* @private
*/
Timeline.prototype._itemFromTarget = function _itemFromTarget (event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-item')) {
return target['timeline-item'];
}
target = target.parentNode;
}
return null;
};

+ 17
- 0
src/timeline/component/Component.js View File

@ -55,6 +55,23 @@ Component.prototype.getOption = function getOption(name) {
return value; return value;
}; };
/**
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/
Component.prototype.setController = function setController (controller) {
this.controller = controller || null;
};
/**
* Get controller of this component
* @return {Controller} controller
*/
Component.prototype.getController = function getController () {
return this.controller;
};
/** /**
* Get the container element of the component, which can be used by a child to * Get the container element of the component, which can be used by a child to
* add its own widgets. Not all components do have a container for childs, in * add its own widgets. Not all components do have a container for childs, in

+ 181
- 0
src/timeline/component/ItemSet.js View File

@ -16,6 +16,13 @@ function ItemSet(parent, depends, options) {
this.parent = parent; this.parent = parent;
this.depends = depends; this.depends = depends;
// event listeners
this.eventListeners = {
dragstart: this._onDragStart.bind(this),
drag: this._onDrag.bind(this),
dragend: this._onDragEnd.bind(this)
};
// one options object is shared by this itemset and all its items // one options object is shared by this itemset and all its items
this.options = options || {}; this.options = options || {};
this.defaultOptions = { this.defaultOptions = {
@ -35,6 +42,7 @@ function ItemSet(parent, depends, options) {
this.itemsData = null; // DataSet this.itemsData = null; // DataSet
this.range = null; // Range or Object {start: number, end: number} this.range = null; // Range or Object {start: number, end: number}
// data change listeners
this.listeners = { this.listeners = {
'add': function (event, params, senderId) { 'add': function (event, params, senderId) {
if (senderId != me.id) { if (senderId != me.id) {
@ -59,6 +67,8 @@ function ItemSet(parent, depends, options) {
this.stack = new Stack(this, Object.create(this.options)); this.stack = new Stack(this, Object.create(this.options));
this.conversion = null; this.conversion = null;
this.touchParams = {}; // stores properties while dragging
// TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
} }
@ -99,6 +109,55 @@ ItemSet.types = {
*/ */
ItemSet.prototype.setOptions = Component.prototype.setOptions; ItemSet.prototype.setOptions = Component.prototype.setOptions;
/**
* Set controller for this component
* @param {Controller | null} controller
*/
ItemSet.prototype.setController = function setController (controller) {
var event;
// unregister old event listeners
if (this.controller) {
for (event in this.eventListeners) {
if (this.eventListeners.hasOwnProperty(event)) {
this.controller.off(event, this.eventListeners[event]);
}
}
}
this.controller = controller || null;
// register new event listeners
if (this.controller) {
for (event in this.eventListeners) {
if (this.eventListeners.hasOwnProperty(event)) {
this.controller.on(event, this.eventListeners[event]);
}
}
}
};
// attach event listeners for dragging items to the controller
(function (me) {
var _controller = null;
var _onDragStart = null;
var _onDrag = null;
var _onDragEnd = null;
Object.defineProperty(me, 'controller', {
get: function () {
return _controller;
},
set: function (controller) {
}
});
}) (this);
/** /**
* Set range (start and end). * Set range (start and end).
* @param {Range | Object} range A Range or an object containing start and end. * @param {Range | Object} range A Range or an object containing start and end.
@ -195,6 +254,7 @@ ItemSet.prototype.repaint = function repaint() {
if (!frame) { if (!frame) {
frame = document.createElement('div'); frame = document.createElement('div');
frame.className = 'itemset'; frame.className = 'itemset';
frame['timeline-itemset'] = this;
var className = options.className; var className = options.className;
if (className) { if (className) {
@ -610,3 +670,124 @@ ItemSet.prototype.toScreen = function toScreen(time) {
var conversion = this.conversion; var conversion = this.conversion;
return (time.valueOf() - conversion.offset) * conversion.scale; return (time.valueOf() - conversion.offset) * conversion.scale;
}; };
/**
* Start dragging the selected events
* @param {Event} event
* @private
*/
ItemSet.prototype._onDragStart = function (event) {
var itemSet = ItemSet.itemSetFromTarget(event),
item = ItemSet.itemFromTarget(event),
me = this;
if (item && item.selected) {
this.touchParams.items = this.getSelection().map(function (id) {
return me.items[id];
});
event.stopPropagation();
}
};
/**
* Drag selected items
* @param {Event} event
* @private
*/
ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.items) {
var deltaX = event.gesture.deltaX;
// adjust the offset of the items being dragged
this.touchParams.items.forEach(function (item) {
item.setOffset(deltaX);
});
// TODO: implement snapping to nice dates
// TODO: implement dragging from one group to another
this.requestReflow();
event.stopPropagation();
}
};
/**
* End of dragging selected items
* @param {Event} event
* @private
*/
ItemSet.prototype._onDragEnd = function (event) {
if (this.touchParams.items) {
var deltaX = event.gesture.deltaX,
scale = this.conversion.scale;
// prepare a changeset for the changed items
var changes = this.touchParams.items.map(function (item) {
item.setOffset(0);
var change = {
id: item.id
};
if ('start' in item.data) {
change.start = new Date(item.data.start.valueOf() + deltaX / scale);
}
if ('end' in item.data) {
change.end = new Date(item.data.end.valueOf() + deltaX / scale);
}
return change;
});
this.touchParams.items = null;
// find the root DataSet from our DataSet/DataView
var data = this.itemsData;
while (data instanceof DataView) {
data = data.data;
}
// apply the changes to the data
data.update(changes);
event.stopPropagation();
}
};
/**
* Find an item from an event target:
* searches for the attribute 'timeline-item' in the event target's element tree
* @param {Event} event
* @return {Item | null} item
*/
ItemSet.itemFromTarget = function itemFromTarget (event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-item')) {
return target['timeline-item'];
}
target = target.parentNode;
}
return null;
};
/**
* Find the ItemSet from an event target:
* searches for the attribute 'timeline-itemset' in the event target's element tree
* @param {Event} event
* @return {ItemSet | null} item
*/
ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-itemset')) {
return target['timeline-itemset'];
}
target = target.parentNode;
}
return null;
};

+ 57
- 46
src/timeline/component/RootPanel.js View File

@ -10,12 +10,29 @@ function RootPanel(container, options) {
this.id = util.randomUUID(); this.id = util.randomUUID();
this.container = container; this.container = container;
// create functions to be used as DOM event listeners
var me = this;
this.hammer = null;
// create listeners for all interesting events, these events will be emitted
// via the controller
var events = [
'touch', 'pinch', 'tap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
];
this.listeners = {};
events.forEach(function (event) {
me.listeners[event] = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
me.controller.emit.apply(me.controller, args);
};
});
this.options = options || {}; this.options = options || {};
this.defaultOptions = { this.defaultOptions = {
autoResize: true autoResize: true
}; };
this.listeners = {}; // event listeners
} }
RootPanel.prototype = new Panel(); RootPanel.prototype = new Panel();
@ -48,6 +65,8 @@ RootPanel.prototype.repaint = function () {
this.frame = frame; this.frame = frame;
this._registerListeners();
changed += 1; changed += 1;
} }
if (!frame.parentNode) { if (!frame.parentNode) {
@ -69,7 +88,6 @@ RootPanel.prototype.repaint = function () {
changed += update(frame.style, 'width', asSize(options.width, '100%')); changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, '100%')); changed += update(frame.style, 'height', asSize(options.height, '100%'));
this._updateEventEmitters();
this._updateWatch(); this._updateWatch();
return (changed > 0); return (changed > 0);
@ -158,58 +176,51 @@ RootPanel.prototype._unwatch = function () {
}; };
/** /**
* Event handler
* @param {String} event name of the event, for example 'click', 'mousemove'
* @param {function} callback callback handler, invoked with the raw HTML Event
* as parameter.
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/ */
RootPanel.prototype.on = function (event, callback) {
// register the listener at this component
var arr = this.listeners[event];
if (!arr) {
arr = [];
this.listeners[event] = arr;
}
arr.push(callback);
RootPanel.prototype.setController = function setController (controller) {
this.controller = controller || null;
this._updateEventEmitters();
if (this.controller) {
this._registerListeners();
}
else {
this._unregisterListeners();
}
}; };
/** /**
* Update the event listeners for all event emitters
* Register event emitters emitted by the rootpanel
* @private * @private
*/ */
RootPanel.prototype._updateEventEmitters = function () {
if (this.listeners) {
var me = this;
util.forEach(this.listeners, function (listeners, event) {
if (!me.emitters) {
me.emitters = {};
RootPanel.prototype._registerListeners = function () {
if (this.frame && this.controller && !this.hammer) {
this.hammer = Hammer(this.frame, {
prevent_default: true
});
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.on(event, this.listeners[event]);
} }
if (!(event in me.emitters)) {
// create event
var frame = me.frame;
if (frame) {
//console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
var callback = function(event) {
listeners.forEach(function (listener) {
// TODO: filter on event target!
listener(event);
});
};
me.emitters[event] = callback;
if (!me.hammer) {
me.hammer = Hammer(frame, {
prevent_default: true
});
}
me.hammer.on(event, callback);
}
}
}
};
/**
* Unregister event emitters from the rootpanel
* @private
*/
RootPanel.prototype._unregisterListeners = function () {
if (this.hammer) {
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.off(event, this.listeners[event]);
} }
});
}
// TODO: be able to delete event listeners
// TODO: be able to move event listeners to a parent when available
this.hammer = null;
} }
}; };

+ 11
- 2
src/timeline/component/item/Item.js View File

@ -20,6 +20,7 @@ function Item (parent, data, options, defaultOptions) {
this.left = 0; this.left = 0;
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.offset = 0;
} }
/** /**
@ -72,10 +73,18 @@ Item.prototype.reflow = function reflow() {
return false; return false;
}; };
/**
* Give the item a display offset in pixels
* @param {Number} offset Offset on screen in pixels
*/
Item.prototype.setOffset = function setOffset(offset) {
this.offset = offset;
};
/** /**
* Return the items width * Return the items width
* @return {Integer} width
* @return {Number} width
*/ */
Item.prototype.getWidth = function getWidth() { Item.prototype.getWidth = function getWidth() {
return this.width; return this.width;
}
};

+ 1
- 1
src/timeline/component/item/ItemBox.js View File

@ -187,7 +187,7 @@ ItemBox.prototype.reflow = function reflow() {
update = util.updateProperty; update = util.updateProperty;
props = this.props; props = this.props;
options = this.options; options = this.options;
start = this.parent.toScreen(this.data.start);
start = this.parent.toScreen(this.data.start) + this.offset;
align = options.align || this.defaultOptions.align; align = options.align || this.defaultOptions.align;
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
orientation = options.orientation || this.defaultOptions.orientation; orientation = options.orientation || this.defaultOptions.orientation;

+ 1
- 1
src/timeline/component/item/ItemPoint.js View File

@ -157,7 +157,7 @@ ItemPoint.prototype.reflow = function reflow() {
options = this.options; options = this.options;
orientation = options.orientation || this.defaultOptions.orientation; orientation = options.orientation || this.defaultOptions.orientation;
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
start = this.parent.toScreen(this.data.start);
start = this.parent.toScreen(this.data.start) + this.offset;
changed += update(this, 'width', dom.point.offsetWidth); changed += update(this, 'width', dom.point.offsetWidth);
changed += update(this, 'height', dom.point.offsetHeight); changed += update(this, 'height', dom.point.offsetHeight);

+ 2
- 2
src/timeline/component/item/ItemRange.js View File

@ -157,8 +157,8 @@ ItemRange.prototype.reflow = function reflow() {
props = this.props; props = this.props;
options = this.options; options = this.options;
parent = this.parent; parent = this.parent;
start = parent.toScreen(this.data.start);
end = parent.toScreen(this.data.end);
start = parent.toScreen(this.data.start) + this.offset;
end = parent.toScreen(this.data.end) + this.offset;
update = util.updateProperty; update = util.updateProperty;
box = dom.box; box = dom.box;
parentWidth = parent.width; parentWidth = parent.width;

+ 154
- 0
src/util.js View File

@ -671,3 +671,157 @@ util.option.asElement = function (value, defaultValue) {
return value || defaultValue || null; return value || defaultValue || null;
}; };
util.GiveDec = function GiveDec(Hex)
{
if(Hex == "A")
Value = 10;
else
if(Hex == "B")
Value = 11;
else
if(Hex == "C")
Value = 12;
else
if(Hex == "D")
Value = 13;
else
if(Hex == "E")
Value = 14;
else
if(Hex == "F")
Value = 15;
else
Value = eval(Hex)
return Value;
}
util.GiveHex = function GiveHex(Dec)
{
if(Dec == 10)
Value = "A";
else
if(Dec == 11)
Value = "B";
else
if(Dec == 12)
Value = "C";
else
if(Dec == 13)
Value = "D";
else
if(Dec == 14)
Value = "E";
else
if(Dec == 15)
Value = "F";
else
Value = "" + Dec;
return Value;
}
/**
* http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
*
* @param {String} hex
* @returns {{r: *, g: *, b: *}}
*/
util.hexToRGB = function hexToRGB(hex) {
hex = hex.replace("#","").toUpperCase();
var a = util.GiveDec(hex.substring(0, 1));
var b = util.GiveDec(hex.substring(1, 2));
var c = util.GiveDec(hex.substring(2, 3));
var d = util.GiveDec(hex.substring(3, 4));
var e = util.GiveDec(hex.substring(4, 5));
var f = util.GiveDec(hex.substring(5, 6));
var r = (a * 16) + b;
var g = (c * 16) + d;
var b = (e * 16) + f;
return {r:r,g:g,b:b};
};
util.RGBToHex = function RGBToHex(red,green,blue) {
var a = util.GiveHex(Math.floor(red / 16));
var b = util.GiveHex(red % 16);
var c = util.GiveHex(Math.floor(green / 16));
var d = util.GiveHex(green % 16);
var e = util.GiveHex(Math.floor(blue / 16));
var f = util.GiveHex(blue % 16);
var hex = a + b + c + d + e + f;
return "#" + hex;
};
/**
* http://www.javascripter.net/faq/rgb2hsv.htm
*
* @param red
* @param green
* @param blue
* @returns {*}
* @constructor
*/
util.RGBToHSV = function RGBToHSV (red,green,blue) {
red=red/255; green=green/255; blue=blue/255;
var minRGB = Math.min(red,Math.min(green,blue));
var maxRGB = Math.max(red,Math.max(green,blue));
// Black-gray-white
if (minRGB == maxRGB) {
return {h:0,s:0,v:minRGB};
}
// Colors other than black-gray-white:
var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
var hue = 60*(h - d/(maxRGB - minRGB))/360;
var saturation = (maxRGB - minRGB)/maxRGB;
var value = maxRGB;
return {h:hue,s:saturation,v:value};
};
/**
* https://gist.github.com/mjijackson/5311256
* @param hue
* @param saturation
* @param value
* @returns {{r: number, g: number, b: number}}
* @constructor
*/
util.HSVToRGB = function HSVToRGB(h, s, v) {
var r, g, b;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
};
util.HSVToHex = function HSVToHex(h,s,v) {
var rgb = util.HSVToRGB(h,s,v);
return util.RGBToHex(rgb.r,rgb.g,rgb.b);
}
util.hexToHSV = function hexToHSV(hex) {
var rgb = util.hexToRGB(hex);
return util.RGBToHSV(rgb.r,rgb.g,rgb.b);
}

Loading…
Cancel
Save