diff --git a/dist/vis.js b/dist/vis.js
index 01389cc3..40366c50 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 3.0.1-SNAPSHOT
- * @date 2014-07-10
+ * @date 2014-07-11
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -88,16 +88,16 @@ return /******/ (function(modules) { // webpackBootstrap
exports.DataView = __webpack_require__(4);
// Graph3d
- exports.Graph3d = __webpack_require__(5);
+ exports.Graph3d = __webpack_require__(11);
// Timeline
- exports.Timeline = __webpack_require__(6);
+ exports.Timeline = __webpack_require__(5);
exports.Graph2d = __webpack_require__(7);
exports.timeline= {
- DataStep: __webpack_require__(8),
- Range: __webpack_require__(9),
- stack: __webpack_require__(10),
- TimeStep: __webpack_require__(11),
+ DataStep: __webpack_require__(6),
+ Range: __webpack_require__(8),
+ stack: __webpack_require__(9),
+ TimeStep: __webpack_require__(10),
components: {
items: {
@@ -1282,7 +1282,6 @@ return /******/ (function(modules) { // webpackBootstrap
*/
exports.binarySearch = function(orderedItems, range, field, field2) {
var array = orderedItems;
- var interval = range.end - range.start;
var found = false;
var low = 0;
@@ -2836,6007 +2835,6007 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
var Emitter = __webpack_require__(41);
+ var Hammer = __webpack_require__(50);
+ var util = __webpack_require__(1);
var DataSet = __webpack_require__(3);
var DataView = __webpack_require__(4);
- var Point3d = __webpack_require__(33);
- var Point2d = __webpack_require__(34);
- var Filter = __webpack_require__(35);
- var StepNumber = __webpack_require__(36);
+ var Range = __webpack_require__(8);
+ var TimeAxis = __webpack_require__(21);
+ var CurrentTime = __webpack_require__(13);
+ var CustomTime = __webpack_require__(14);
+ var ItemSet = __webpack_require__(18);
/**
- * @constructor Graph3d
- * Graph3d displays data in 3d.
- *
- * Graph3d is developed in javascript as a Google Visualization Chart.
- *
- * @param {Element} container The DOM element in which the Graph3d will
- * be created. Normally a div element.
- * @param {DataSet | DataView | Array} [data]
- * @param {Object} [options]
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
*/
- function Graph3d(container, data, options) {
- if (!(this instanceof Graph3d)) {
+ function Timeline (container, items, options) {
+ if (!(this instanceof Timeline)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
- // create variables and set default values
- this.containerElement = container;
- this.width = '400px';
- this.height = '400px';
- this.margin = 10; // px
- this.defaultXCenter = '55%';
- this.defaultYCenter = '50%';
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
- this.xLabel = 'x';
- this.yLabel = 'y';
- this.zLabel = 'z';
- this.filterLabel = 'time';
- this.legendLabel = 'value';
+ autoResize: true,
- this.style = Graph3d.STYLE.DOT;
- this.showPerspective = true;
- this.showGrid = true;
- this.keepAspectRatio = true;
- this.showShadow = false;
- this.showGrayBottom = false; // TODO: this does not work correctly
- this.showTooltip = false;
- this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
+ orientation: 'bottom',
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
- this.animationInterval = 1000; // milliseconds
- this.animationPreload = false;
+ // Create the DOM, props, and emitter
+ this._create(container);
- this.camera = new Graph3d.Camera();
- this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
+ // all components listed here will be repainted automatically
+ this.components = [];
- this.dataTable = null; // The original data table
- this.dataPoints = null; // The table with point objects
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
+ }
+ };
- // the column indexes
- this.colX = undefined;
- this.colY = undefined;
- this.colZ = undefined;
- this.colValue = undefined;
- this.colFilter = undefined;
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
- this.xMin = 0;
- this.xStep = undefined; // auto by default
- this.xMax = 1;
- this.yMin = 0;
- this.yStep = undefined; // auto by default
- this.yMax = 1;
- this.zMin = 0;
- this.zStep = undefined; // auto by default
- this.zMax = 1;
- this.valueMin = 0;
- this.valueMax = 1;
- this.xBarWidth = 1;
- this.yBarWidth = 1;
- // TODO: customize axis range
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
- // constants
- this.colorAxis = '#4D4D4D';
- this.colorGrid = '#D3D3D3';
- this.colorDot = '#7DC1FF';
- this.colorDotBorder = '#3267D2';
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
- // create a frame and canvas
- this.create();
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- // apply options (also when undefined)
- this.setOptions(options);
+ // item set
+ this.itemSet = new ItemSet(this.body);
+ this.components.push(this.itemSet);
- // apply data
- if (data) {
- this.setData(data);
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // apply options
+ if (options) {
+ this.setOptions(options);
+ }
+
+ // create itemset
+ if (items) {
+ this.setItems(items);
+ }
+ else {
+ this.redraw();
}
}
- // Extend Graph3d with an Emitter mixin
- Emitter(Graph3d.prototype);
+ // turn Timeline into an event emitter
+ Emitter(Timeline.prototype);
/**
- * @class Camera
- * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
- * The camera is always looking in the direction of the origin of the arm.
- * This way, the camera always rotates around one fixed point, the location
- * of the camera arm.
- *
- * Documentation:
- * http://en.wikipedia.org/wiki/3D_projection
+ * Create the main DOM for the Timeline: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Timeline will
+ * be attached.
+ * @private
*/
- Graph3d.Camera = function () {
- this.armLocation = new Point3d();
- this.armRotation = {};
- this.armRotation.horizontal = 0;
- this.armRotation.vertical = 0;
- this.armLength = 1.7;
+ Timeline.prototype._create = function (container) {
+ this.dom = {};
- this.cameraLocation = new Point3d();
- this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
- this.calculateCameraOrientation();
- };
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
- /**
- * Set the location (origin) of the arm
- * @param {Number} x Normalized value of x
- * @param {Number} y Normalized value of y
- * @param {Number} z Normalized value of z
- */
- Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
- this.armLocation.x = x;
- this.armLocation.y = y;
- this.armLocation.z = z;
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontal);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
- this.calculateCameraOrientation();
- };
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
- /**
- * Set the rotation of the camera arm
- * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
- * Optional, can be left undefined.
- * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
- * if vertical=0.5*PI, the graph is shown from the
- * top. Optional, can be left undefined.
- */
- Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
- if (horizontal !== undefined) {
- this.armRotation.horizontal = horizontal;
- }
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
- if (vertical !== undefined) {
- this.armRotation.vertical = vertical;
- if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
- if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
- }
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('change', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.bind(this));
- if (horizontal !== undefined || vertical !== undefined) {
- this.calculateCameraOrientation();
- }
- };
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.dom.root, {
+ prevent_default: true
+ });
+ this.listeners = {};
- /**
- * Retrieve the current arm rotation
- * @return {object} An object with parameters horizontal and vertical
- */
- Graph3d.Camera.prototype.getArmRotation = function() {
- var rot = {};
- rot.horizontal = this.armRotation.horizontal;
- rot.vertical = this.armRotation.vertical;
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
+ ];
+ events.forEach(function (event) {
+ var listener = function () {
+ var args = [event].concat(Array.prototype.slice.call(arguments, 0));
+ me.emit.apply(me, args);
+ };
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
- return rot;
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
+
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
};
/**
- * Set the (normalized) length of the camera arm.
- * @param {Number} length A length between 0.71 and 5.0
+ * Destroy the Timeline, clean up all DOM elements and event listeners.
*/
- Graph3d.Camera.prototype.setArmLength = function(length) {
- if (length === undefined)
- return;
+ Timeline.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
- this.armLength = length;
+ // remove all event listeners
+ this.off();
- // Radius must be larger than the corner of the graph,
- // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
- // graph
- if (this.armLength < 0.71) this.armLength = 0.71;
- if (this.armLength > 5.0) this.armLength = 5.0;
+ // stop checking for changed size
+ this._stopAutoResize();
- this.calculateCameraOrientation();
- };
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
+ }
+ this.dom = null;
- /**
- * Retrieve the arm length
- * @return {Number} length
- */
- Graph3d.Camera.prototype.getArmLength = function() {
- return this.armLength;
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
+ }
+ }
+ this.listeners = null;
+ this.hammer = null;
+
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
+
+ this.body = null;
};
/**
- * Retrieve the camera location
- * @return {Point3d} cameraLocation
+ * Set options. Options will be passed to all components loaded in the Timeline.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Timeline,
+ * can be 'bottom' (default) or 'top'.
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Timeline will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
*/
- Graph3d.Camera.prototype.getCameraLocation = function() {
- return this.cameraLocation;
+ Timeline.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
+ util.selectiveExtend(fields, this.options, options);
+
+ // enable/disable autoResize
+ this._initAutoResize();
+ }
+
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
+
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
+ if (options && options.order) {
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
+ }
+
+ // redraw everything
+ this.redraw();
};
/**
- * Retrieve the camera rotation
- * @return {Point3d} cameraRotation
+ * Set a custom time bar
+ * @param {Date} time
*/
- Graph3d.Camera.prototype.getCameraRotation = function() {
- return this.cameraRotation;
+ Timeline.prototype.setCustomTime = function (time) {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
+
+ this.customTime.setCustomTime(time);
};
/**
- * Calculate the location and rotation of the camera based on the
- * position and orientation of the camera arm
+ * Retrieve the current custom time.
+ * @return {Date} customTime
*/
- Graph3d.Camera.prototype.calculateCameraOrientation = function() {
- // calculate location of the camera
- this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
- this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
- this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
+ Timeline.prototype.getCustomTime = function() {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
- // calculate rotation of the camera
- this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
- this.cameraRotation.y = 0;
- this.cameraRotation.z = -this.armRotation.horizontal;
+ return this.customTime.getCustomTime();
};
/**
- * Calculate the scaling values, dependent on the range in x, y, and z direction
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
- Graph3d.prototype._setScale = function() {
- this.scale = new Point3d(1 / (this.xMax - this.xMin),
- 1 / (this.yMax - this.yMin),
- 1 / (this.zMax - this.zMin));
+ Timeline.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
- // keep aspect ration between x and y scale if desired
- if (this.keepAspectRatio) {
- if (this.scale.x < this.scale.y) {
- //noinspection JSSuspiciousNameCombination
- this.scale.y = this.scale.x;
- }
- else {
- //noinspection JSSuspiciousNameCombination
- this.scale.x = this.scale.y;
- }
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
}
- // scale the vertical axis
- this.scale.z *= this.verticalRatio;
- // TODO: can this be automated? verticalRatio?
-
- // determine scale for (optional) value
- this.scale.value = 1 / (this.valueMax - this.valueMin);
+ // set items
+ this.itemsData = newDataSet;
+ this.itemSet && this.itemSet.setItems(newDataSet);
- // position the camera arm
- var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
- var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
- var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
- this.camera.setArmLocation(xCenter, yCenter, zCenter);
- };
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
+ this.fit();
+ var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
+ var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
- /**
- * Convert a 3D location to a 2D location on screen
- * http://en.wikipedia.org/wiki/3D_projection
- * @param {Point3d} point3d A 3D point with parameters x, y, z
- * @return {Point2d} point2d A 2D point with parameters x, y
- */
- Graph3d.prototype._convert3Dto2D = function(point3d) {
- var translation = this._convertPointToTranslation(point3d);
- return this._convertTranslationToScreen(translation);
+ this.setWindow(start, end);
+ }
};
/**
- * Convert a 3D location its translation seen from the camera
- * http://en.wikipedia.org/wiki/3D_projection
- * @param {Point3d} point3d A 3D point with parameters x, y, z
- * @return {Point3d} translation A 3D point with parameters x, y, z This is
- * the translation of the point, seen from the
- * camera
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
- Graph3d.prototype._convertPointToTranslation = function(point3d) {
- var ax = point3d.x * this.scale.x,
- ay = point3d.y * this.scale.y,
- az = point3d.z * this.scale.z,
+ Timeline.prototype.setGroups = function(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
- cx = this.camera.getCameraLocation().x,
- cy = this.camera.getCameraLocation().y,
- cz = this.camera.getCameraLocation().z,
+ this.groupsData = newDataSet;
+ this.itemSet.setGroups(newDataSet);
+ };
- // calculate angles
- sinTx = Math.sin(this.camera.getCameraRotation().x),
- cosTx = Math.cos(this.camera.getCameraRotation().x),
- sinTy = Math.sin(this.camera.getCameraRotation().y),
- cosTy = Math.cos(this.camera.getCameraRotation().y),
- sinTz = Math.sin(this.camera.getCameraRotation().z),
- cosTz = Math.cos(this.camera.getCameraRotation().z),
+ /**
+ * Clear the Timeline. By Default, items, groups and options are cleared.
+ * Example usage:
+ *
+ * timeline.clear(); // clear items, groups, and options
+ * timeline.clear({options: true}); // clear options only
+ *
+ * @param {Object} [what] Optionally specify what to clear. By default:
+ * {items: true, groups: true, options: true}
+ */
+ Timeline.prototype.clear = function(what) {
+ // clear items
+ if (!what || what.items) {
+ this.setItems(null);
+ }
- // calculate translation
- dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
- dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
- dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
+ // clear groups
+ if (!what || what.groups) {
+ this.setGroups(null);
+ }
- return new Point3d(dx, dy, dz);
+ // clear options of timeline and of each of the components
+ if (!what || what.options) {
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ });
+
+ this.setOptions(this.defaultOptions); // this will also do a redraw
+ }
};
/**
- * Convert a translation point to a point on the screen
- * @param {Point3d} translation A 3D point with parameters x, y, z This is
- * the translation of the point, seen from the
- * camera
- * @return {Point2d} point2d A 2D point with parameters x, y
+ * Set Timeline window such that it fits all items
*/
- Graph3d.prototype._convertTranslationToScreen = function(translation) {
- var ex = this.eye.x,
- ey = this.eye.y,
- ez = this.eye.z,
- dx = translation.x,
- dy = translation.y,
- dz = translation.z;
+ Timeline.prototype.fit = function() {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
- // calculate position on screen from translation
- var bx;
- var by;
- if (this.showPerspective) {
- bx = (dx - ex) * (ez / dz);
- by = (dy - ey) * (ez / dz);
+ // add 5% space on both sides
+ var start = dataRange.min;
+ var end = dataRange.max;
+ if (start != null && end != null) {
+ var interval = (end.valueOf() - start.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ start = new Date(start.valueOf() - interval * 0.05);
+ end = new Date(end.valueOf() + interval * 0.05);
}
- else {
- bx = dx * -(ez / this.camera.getArmLength());
- by = dy * -(ez / this.camera.getArmLength());
+
+ // skip range set if there is no start and end date
+ if (start === null && end === null) {
+ return;
}
- // shift and scale the point to the center of the screen
- // use the width of the graph to scale both horizontally and vertically.
- return new Point2d(
- this.xcenter + bx * this.frame.canvas.clientWidth,
- this.ycenter - by * this.frame.canvas.clientWidth);
+ this.range.setRange(start, end);
};
/**
- * Set the background styling for the graph
- * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
*/
- Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
- var fill = 'white';
- var stroke = 'gray';
- var strokeWidth = 1;
+ Timeline.prototype.getItemRange = function() {
+ // calculate min from start filed
+ var dataset = this.itemsData.getDataSet(),
+ min = null,
+ max = null;
- if (typeof(backgroundColor) === 'string') {
- fill = backgroundColor;
- stroke = 'none';
- strokeWidth = 0;
- }
- else if (typeof(backgroundColor) === 'object') {
- if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
- if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
- if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
- }
- else if (backgroundColor === undefined) {
- // use use defaults
- }
- else {
- throw 'Unsupported type of backgroundColor';
+ if (dataset) {
+ // calculate the minimum value of the field 'start'
+ var minItem = dataset.min('start');
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
+
+ // calculate maximum value of fields 'start' and 'end'
+ var maxStartItem = dataset.max('start');
+ if (maxStartItem) {
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
+ }
+ var maxEndItem = dataset.max('end');
+ if (maxEndItem) {
+ if (max == null) {
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
+ }
+ else {
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
+ }
+ }
}
- this.frame.style.backgroundColor = fill;
- this.frame.style.borderColor = stroke;
- this.frame.style.borderWidth = strokeWidth + 'px';
- this.frame.style.borderStyle = 'solid';
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
};
+ /**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ */
+ Timeline.prototype.setSelection = function(ids) {
+ this.itemSet && this.itemSet.setSelection(ids);
+ };
- /// enumerate the available styles
- Graph3d.STYLE = {
- BAR: 0,
- BARCOLOR: 1,
- BARSIZE: 2,
- DOT : 3,
- DOTLINE : 4,
- DOTCOLOR: 5,
- DOTSIZE: 6,
- GRID : 7,
- LINE: 8,
- SURFACE : 9
+ /**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ Timeline.prototype.getSelection = function() {
+ return this.itemSet && this.itemSet.getSelection() || [];
};
/**
- * Retrieve the style index from given styleName
- * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
- * @return {Number} styleNumber Enumeration value representing the style, or -1
- * when not found
+ * Set the visible window. Both parameters are optional, you can change only
+ * start or only end. Syntax:
+ *
+ * TimeLine.setWindow(start, end)
+ * TimeLine.setWindow(range)
+ *
+ * Where start and end can be a Date, number, or string, and range is an
+ * object with properties start and end.
+ *
+ * @param {Date | Number | String | Object} [start] Start date of visible window
+ * @param {Date | Number | String} [end] End date of visible window
*/
- Graph3d.prototype._getStyleNumber = function(styleName) {
- switch (styleName) {
- case 'dot': return Graph3d.STYLE.DOT;
- case 'dot-line': return Graph3d.STYLE.DOTLINE;
- case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
- case 'dot-size': return Graph3d.STYLE.DOTSIZE;
- case 'line': return Graph3d.STYLE.LINE;
- case 'grid': return Graph3d.STYLE.GRID;
- case 'surface': return Graph3d.STYLE.SURFACE;
- case 'bar': return Graph3d.STYLE.BAR;
- case 'bar-color': return Graph3d.STYLE.BARCOLOR;
- case 'bar-size': return Graph3d.STYLE.BARSIZE;
+ Timeline.prototype.setWindow = function(start, end) {
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end);
+ }
+ else {
+ this.range.setRange(start, end);
}
+ };
- return -1;
+ /**
+ * Get the visible window
+ * @return {{start: Date, end: Date}} Visible range
+ */
+ Timeline.prototype.getWindow = function() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
};
/**
- * Determine the indexes of the data columns, based on the given style and data
- * @param {DataSet} data
- * @param {Number} style
+ * Force a redraw of the Timeline. Can be useful to manually redraw when
+ * option autoResize=false
*/
- Graph3d.prototype._determineColumnIndexes = function(data, style) {
- if (this.style === Graph3d.STYLE.DOT ||
- this.style === Graph3d.STYLE.DOTLINE ||
- this.style === Graph3d.STYLE.LINE ||
- this.style === Graph3d.STYLE.GRID ||
- this.style === Graph3d.STYLE.SURFACE ||
- this.style === Graph3d.STYLE.BAR) {
- // 3 columns expected, and optionally a 4th with filter values
- this.colX = 0;
- this.colY = 1;
- this.colZ = 2;
- this.colValue = undefined;
+ Timeline.prototype.redraw = function() {
+ var resized = false,
+ options = this.options,
+ props = this.props,
+ dom = this.dom;
- if (data.getNumberOfColumns() > 3) {
- this.colFilter = 3;
- }
- }
- else if (this.style === Graph3d.STYLE.DOTCOLOR ||
- this.style === Graph3d.STYLE.DOTSIZE ||
- this.style === Graph3d.STYLE.BARCOLOR ||
- this.style === Graph3d.STYLE.BARSIZE) {
- // 4 columns expected, and optionally a 5th with filter values
- this.colX = 0;
- this.colY = 1;
- this.colZ = 2;
- this.colValue = 3;
+ if (!dom) return; // when destroyed
- if (data.getNumberOfColumns() > 4) {
- this.colFilter = 4;
- }
- }
- else {
- throw 'Unknown style "' + this.style + '"';
- }
- };
+ // update class names
+ dom.root.className = 'vis timeline root ' + options.orientation;
- Graph3d.prototype.getNumberOfRows = function(data) {
- return data.length;
- }
+ // update root width and height options
+ dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
+ dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
- Graph3d.prototype.getNumberOfColumns = function(data) {
- var counter = 0;
- for (var column in data[0]) {
- if (data[0].hasOwnProperty(column)) {
- counter++;
- }
- }
- return counter;
- }
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+ // TODO: compensate borders when any of the panels is empty.
- Graph3d.prototype.getDistinctValues = function(data, column) {
- var distinctValues = [];
- for (var i = 0; i < data.length; i++) {
- if (distinctValues.indexOf(data[i][column]) == -1) {
- distinctValues.push(data[i][column]);
- }
- }
- return distinctValues;
- }
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
- Graph3d.prototype.getColumnRange = function(data,column) {
- var minMax = {min:data[0][column],max:data[0][column]};
- for (var i = 0; i < data.length; i++) {
- if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
- if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
- }
- return minMax;
- };
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
- /**
- * Initialize the data from the data table. Calculate minimum and maximum values
- * and column index values
- * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
- * @param {Number} style Style Number
- */
- Graph3d.prototype._dataInitialize = function (rawData, style) {
- var me = this;
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
- // unsubscribe from the dataTable
- if (this.dataSet) {
- this.dataSet.off('*', this._onChange);
- }
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
- if (rawData === undefined)
- return;
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = props.left.width + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
- if (Array.isArray(rawData)) {
- rawData = new DataSet(rawData);
- }
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Timeline or of the contents of the center changed
+ this._updateScrollTop();
- var data;
- if (rawData instanceof DataSet || rawData instanceof DataView) {
- data = rawData.get();
- }
- else {
- throw new Error('Array, DataSet, or DataView expected');
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height -
+ this.props.border.top - this.props.border.bottom, 0);
}
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
- if (data.length == 0)
- return;
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
- this.dataSet = rawData;
- this.dataTable = data;
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep repainting until all sizes are settled
+ this.redraw();
+ }
+ };
- // subscribe to changes in the dataset
- this._onChange = function () {
- me.setData(me.dataSet);
- };
- this.dataSet.on('*', this._onChange);
+ // TODO: deprecated since version 1.1.0, remove some day
+ Timeline.prototype.repaint = function () {
+ throw new Error('Function repaint is deprecated. Use redraw instead.');
+ };
- // _determineColumnIndexes
- // getNumberOfRows (points)
- // getNumberOfColumns (x,y,z,v,t,t1,t2...)
- // getDistinctValues (unique values?)
- // getColumnRange
+ /**
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
+ */
+ // TODO: move this function to Range
+ Timeline.prototype._toTime = function(x) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return new Date(x / conversion.scale + conversion.offset);
+ };
- // determine the location of x,y,z,value,filter columns
- this.colX = 'x';
- this.colY = 'y';
- this.colZ = 'z';
- this.colValue = 'style';
- this.colFilter = 'filter';
+ /**
+ * Convert a position on the global screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
+ */
+ // TODO: move this function to Range
+ Timeline.prototype._toGlobalTime = function(x) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return new Date(x / conversion.scale + conversion.offset);
+ };
+ /**
+ * Convert a datetime (Date object) into a position on the screen
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+ // TODO: move this function to Range
+ Timeline.prototype._toScreen = function(time) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+ };
- // check if a filter column is provided
- if (data[0].hasOwnProperty('filter')) {
- if (this.dataFilter === undefined) {
- this.dataFilter = new Filter(rawData, this.colFilter, this);
- this.dataFilter.setOnLoadCallback(function() {me.redraw();});
- }
- }
-
-
- var withBars = this.style == Graph3d.STYLE.BAR ||
- this.style == Graph3d.STYLE.BARCOLOR ||
- this.style == Graph3d.STYLE.BARSIZE;
-
- // determine barWidth from data
- if (withBars) {
- if (this.defaultXBarWidth !== undefined) {
- this.xBarWidth = this.defaultXBarWidth;
- }
- else {
- var dataX = this.getDistinctValues(data,this.colX);
- this.xBarWidth = (dataX[1] - dataX[0]) || 1;
- }
- if (this.defaultYBarWidth !== undefined) {
- this.yBarWidth = this.defaultYBarWidth;
- }
- else {
- var dataY = this.getDistinctValues(data,this.colY);
- this.yBarWidth = (dataY[1] - dataY[0]) || 1;
- }
- }
+ /**
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+ // TODO: move this function to Range
+ Timeline.prototype._toGlobalScreen = function(time) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+ };
- // calculate minimums and maximums
- var xRange = this.getColumnRange(data,this.colX);
- if (withBars) {
- xRange.min -= this.xBarWidth / 2;
- xRange.max += this.xBarWidth / 2;
- }
- this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
- this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
- if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
- this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
- var yRange = this.getColumnRange(data,this.colY);
- if (withBars) {
- yRange.min -= this.yBarWidth / 2;
- yRange.max += this.yBarWidth / 2;
+ /**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+ Timeline.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
}
- this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
- this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
- if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
- this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
-
- var zRange = this.getColumnRange(data,this.colZ);
- this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
- this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
- if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
- this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
-
- if (this.colValue !== undefined) {
- var valueRange = this.getColumnRange(data,this.colValue);
- this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
- this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
- if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
+ else {
+ this._stopAutoResize();
}
-
- // set the scale dependent on the ranges.
- this._setScale();
};
-
-
/**
- * Filter the data based on the current filter
- * @param {Array} data
- * @return {Array} dataPoints Array with point objects which can be drawn on screen
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
*/
- Graph3d.prototype._getDataPoints = function (data) {
- // TODO: store the created matrix dataPoints in the filters instead of reloading each time
- var x, y, i, z, obj, point;
+ Timeline.prototype._startAutoResize = function () {
+ var me = this;
- var dataPoints = [];
+ this._stopAutoResize();
- if (this.style === Graph3d.STYLE.GRID ||
- this.style === Graph3d.STYLE.SURFACE) {
- // copy all values from the google data table to a matrix
- // the provided values are supposed to form a grid of (x,y) positions
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
+ }
- // create two lists with all present x and y values
- var dataX = [];
- var dataY = [];
- for (i = 0; i < this.getNumberOfRows(data); i++) {
- x = data[i][this.colX] || 0;
- y = data[i][this.colY] || 0;
+ if (me.dom.root) {
+ // check whether the frame is resized
+ if ((me.dom.root.clientWidth != me.props.lastWidth) ||
+ (me.dom.root.clientHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.clientWidth;
+ me.props.lastHeight = me.dom.root.clientHeight;
- if (dataX.indexOf(x) === -1) {
- dataX.push(x);
- }
- if (dataY.indexOf(y) === -1) {
- dataY.push(y);
+ me.emit('change');
}
}
+ };
- function sortNumber(a, b) {
- return a - b;
- }
- dataX.sort(sortNumber);
- dataY.sort(sortNumber);
-
- // create a grid, a 2d matrix, with all values.
- var dataMatrix = []; // temporary data matrix
- for (i = 0; i < data.length; i++) {
- x = data[i][this.colX] || 0;
- y = data[i][this.colY] || 0;
- z = data[i][this.colZ] || 0;
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
- var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
- var yIndex = dataY.indexOf(y);
+ this.watchTimer = setInterval(this._onResize, 1000);
+ };
- if (dataMatrix[xIndex] === undefined) {
- dataMatrix[xIndex] = [];
- }
+ /**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+ Timeline.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
- var point3d = new Point3d();
- point3d.x = x;
- point3d.y = y;
- point3d.z = z;
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
+ };
- obj = {};
- obj.point = point3d;
- obj.trans = undefined;
- obj.screen = undefined;
- obj.bottom = new Point3d(x, y, this.zMin);
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Timeline.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
+ };
- dataMatrix[xIndex][yIndex] = obj;
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Timeline.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
+ };
- dataPoints.push(obj);
- }
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Timeline.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+ };
- // fill in the pointers to the neighbors.
- for (x = 0; x < dataMatrix.length; x++) {
- for (y = 0; y < dataMatrix[x].length; y++) {
- if (dataMatrix[x][y]) {
- dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
- dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
- dataMatrix[x][y].pointCross =
- (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
- dataMatrix[x+1][y+1] :
- undefined;
- }
- }
- }
- }
- else { // 'dot', 'dot-line', etc.
- // copy all values from the google data table to a list with Point3d objects
- for (i = 0; i < data.length; i++) {
- point = new Point3d();
- point.x = data[i][this.colX] || 0;
- point.y = data[i][this.colY] || 0;
- point.z = data[i][this.colZ] || 0;
+ /**
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Timeline.prototype._onDrag = function (event) {
+ // 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
+ if (!this.touch.allowDragging) return;
- if (this.colValue !== undefined) {
- point.value = data[i][this.colValue] || 0;
- }
+ var delta = event.gesture.deltaY;
- obj = {};
- obj.point = point;
- obj.bottom = new Point3d(point.x, point.y, this.zMin);
- obj.trans = undefined;
- obj.screen = undefined;
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
- dataPoints.push(obj);
- }
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
}
-
- return dataPoints;
};
/**
- * Create the main frame for the Graph3d.
- * This function is executed once when a Graph3d object is created. The frame
- * contains a canvas, and this canvas contains all objects like the axis and
- * nodes.
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
*/
- Graph3d.prototype.create = function () {
- // remove all elements from the container element.
- while (this.containerElement.hasChildNodes()) {
- this.containerElement.removeChild(this.containerElement.firstChild);
- }
-
- this.frame = document.createElement('div');
- this.frame.style.position = 'relative';
- this.frame.style.overflow = 'hidden';
+ Timeline.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
+ };
- // create the graph canvas (HTML canvas element)
- this.frame.canvas = document.createElement( 'canvas' );
- this.frame.canvas.style.position = 'relative';
- this.frame.appendChild(this.frame.canvas);
- //if (!this.frame.canvas.getContext) {
- {
- var noCanvas = document.createElement( 'DIV' );
- noCanvas.style.color = 'red';
- noCanvas.style.fontWeight = 'bold' ;
- noCanvas.style.padding = '10px';
- noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
- this.frame.canvas.appendChild(noCanvas);
+ /**
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+ Timeline.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ }
+ this.props.scrollTopMin = scrollTopMin;
}
- this.frame.filter = document.createElement( 'div' );
- this.frame.filter.style.position = 'absolute';
- this.frame.filter.style.bottom = '0px';
- this.frame.filter.style.left = '0px';
- this.frame.filter.style.width = '100%';
- this.frame.appendChild(this.frame.filter);
-
- // add event listeners to handle moving and zooming the contents
- var me = this;
- var onmousedown = function (event) {me._onMouseDown(event);};
- var ontouchstart = function (event) {me._onTouchStart(event);};
- var onmousewheel = function (event) {me._onWheel(event);};
- var ontooltip = function (event) {me._onTooltip(event);};
- // TODO: these events are never cleaned up... can give a 'memory leakage'
-
- G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
- G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
- G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
- G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
- G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
- // add the new graph to the container element
- this.containerElement.appendChild(this.frame);
+ return this.props.scrollTop;
};
-
/**
- * Set a new size for the graph
- * @param {string} width Width in pixels or percentage (for example '800px'
- * or '50%')
- * @param {string} height Height in pixels or percentage (for example '400px'
- * or '30%')
+ * Get the current scrollTop
+ * @returns {number} scrollTop
+ * @private
*/
- Graph3d.prototype.setSize = function(width, height) {
- this.frame.style.width = width;
- this.frame.style.height = height;
-
- this._resizeCanvas();
+ Timeline.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
};
- /**
- * Resize the canvas to the current size of the frame
- */
- Graph3d.prototype._resizeCanvas = function() {
- this.frame.canvas.style.width = '100%';
- this.frame.canvas.style.height = '100%';
+ module.exports = Timeline;
- this.frame.canvas.width = this.frame.canvas.clientWidth;
- this.frame.canvas.height = this.frame.canvas.clientHeight;
- // adjust with for margin
- this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
- };
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Start animation
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
+ *
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- Graph3d.prototype.animationStart = function() {
- if (!this.frame.filter || !this.frame.filter.slider)
- throw 'No animation available';
+ function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
+ // variables
+ this.current = 0;
- this.frame.filter.slider.play();
- };
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
+ this.marginStart;
+ this.marginEnd;
- /**
- * Stop animation
- */
- Graph3d.prototype.animationStop = function() {
- if (!this.frame.filter || !this.frame.filter.slider) return;
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
+
+ this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
+ }
- this.frame.filter.slider.stop();
- };
/**
- * Resize the center position based on the current values in this.defaultXCenter
- * and this.defaultYCenter (which are strings with a percentage or a value
- * in pixels). The center positions are the variables this.xCenter
- * and this.yCenter
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- Graph3d.prototype._resizeCenter = function() {
- // calculate the horizontal center position
- if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
- this.xcenter =
- parseFloat(this.defaultXCenter) / 100 *
- this.frame.canvas.clientWidth;
- }
- else {
- this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
- }
+ DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
+ this._start = start;
+ this._end = end;
- // calculate the vertical center position
- if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
- this.ycenter =
- parseFloat(this.defaultYCenter) / 100 *
- (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
- }
- else {
- this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
}
+ this.setFirst();
};
/**
- * Set the rotation and distance of the camera
- * @param {Object} pos An object with the camera position. The object
- * contains three parameters:
- * - horizontal {Number}
- * The horizontal rotation, between 0 and 2*PI.
- * Optional, can be left undefined.
- * - vertical {Number}
- * The vertical rotation, between 0 and 0.5*PI
- * if vertical=0.5*PI, the graph is shown from the
- * top. Optional, can be left undefined.
- * - distance {Number}
- * The (normalized) distance of the camera to the
- * center of the graph, a value between 0.71 and 5.0.
- * Optional, can be left undefined.
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- Graph3d.prototype.setCameraPosition = function(pos) {
- if (pos === undefined) {
- return;
- }
+ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.1;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
- if (pos.horizontal !== undefined && pos.vertical !== undefined) {
- this.camera.setArmRotation(pos.horizontal, pos.vertical);
- }
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
- if (pos.distance !== undefined) {
- this.camera.setArmLength(pos.distance);
+ var start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
}
- this.redraw();
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
+ }
+ }
+ if (solutionFound == true) {
+ break;
+ }
+ }
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
/**
- * Retrieve the current camera rotation
- * @return {object} An object with parameters horizontal, vertical, and
- * distance
+ * Set the range iterator to the start date.
*/
- Graph3d.prototype.getCameraPosition = function() {
- var pos = this.camera.getArmRotation();
- pos.distance = this.camera.getArmLength();
- return pos;
+ DataStep.prototype.first = function() {
+ this.setFirst();
};
/**
- * Load data into the 3D Graph
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
- Graph3d.prototype._readData = function(data) {
- // read the data
- this._dataInitialize(data, this.style);
+ DataStep.prototype.setFirst = function() {
+ var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
+ var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
+ this.marginEnd = this.roundToMinor(niceEnd);
+ this.marginStart = this.roundToMinor(niceStart);
+ this.marginRange = this.marginEnd - this.marginStart;
- if (this.dataFilter) {
- // apply filtering
- this.dataPoints = this.dataFilter._getDataPoints();
+ this.current = this.marginEnd;
+
+ };
+
+ DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
else {
- // no filtering. load all data
- this.dataPoints = this._getDataPoints(this.dataTable);
+ return rounded;
}
+ }
- // draw the filter
- this._redrawFilter();
+
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
};
/**
- * Replace the dataset of the Graph3d
- * @param {Array | DataSet | DataView} data
+ * Do the next step
*/
- Graph3d.prototype.setData = function (data) {
- this._readData(data);
- this.redraw();
+ DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
- // start animation when option is true
- if (this.animationAutoStart && this.dataFilter) {
- this.animationStart();
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
}
};
/**
- * Update the options. Options will be merged with current options
- * @param {Object} options
+ * Do the next step
*/
- Graph3d.prototype.setOptions = function (options) {
- var cameraPosition = undefined;
-
- this.animationStop();
-
- if (options !== undefined) {
- // retrieve parameter values
- if (options.width !== undefined) this.width = options.width;
- if (options.height !== undefined) this.height = options.height;
+ DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+ };
- if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
- if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
- if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
- if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
- if (options.xLabel !== undefined) this.xLabel = options.xLabel;
- if (options.yLabel !== undefined) this.yLabel = options.yLabel;
- if (options.zLabel !== undefined) this.zLabel = options.zLabel;
- if (options.style !== undefined) {
- var styleNumber = this._getStyleNumber(options.style);
- if (styleNumber !== -1) {
- this.style = styleNumber;
- }
+ /**
+ * Get the current datetime
+ * @return {String} current The current date
+ */
+ DataStep.prototype.getCurrent = function() {
+ var toPrecision = '' + Number(this.current).toPrecision(5);
+ for (var i = toPrecision.length-1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0,i);
}
- if (options.showGrid !== undefined) this.showGrid = options.showGrid;
- if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
- if (options.showShadow !== undefined) this.showShadow = options.showShadow;
- if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
- if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
- if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
- if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
-
- if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
- if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
- if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
-
- if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
- if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
-
- if (options.xMin !== undefined) this.defaultXMin = options.xMin;
- if (options.xStep !== undefined) this.defaultXStep = options.xStep;
- if (options.xMax !== undefined) this.defaultXMax = options.xMax;
- if (options.yMin !== undefined) this.defaultYMin = options.yMin;
- if (options.yStep !== undefined) this.defaultYStep = options.yStep;
- if (options.yMax !== undefined) this.defaultYMax = options.yMax;
- if (options.zMin !== undefined) this.defaultZMin = options.zMin;
- if (options.zStep !== undefined) this.defaultZStep = options.zStep;
- if (options.zMax !== undefined) this.defaultZMax = options.zMax;
- if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
- if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
-
- if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
-
- if (cameraPosition !== undefined) {
- this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
- this.camera.setArmLength(cameraPosition.distance);
+ else if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0,i);
+ break;
}
- else {
- this.camera.setArmRotation(1.0, 0.5);
- this.camera.setArmLength(1.7);
+ else{
+ break;
}
}
- this._setBackgroundColor(options && options.backgroundColor);
-
- this.setSize(this.width, this.height);
+ return toPrecision;
+ };
- // re-load the data
- if (this.dataTable) {
- this.setData(this.dataTable);
- }
- // start animation when option is true
- if (this.animationAutoStart && this.dataFilter) {
- this.animationStart();
- }
- };
/**
- * Redraw the Graph.
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
- Graph3d.prototype.redraw = function() {
- if (this.dataPoints === undefined) {
- throw 'Error: graph data not initialized';
- }
-
- this._resizeCanvas();
- this._resizeCenter();
- this._redrawSlider();
- this._redrawClear();
- this._redrawAxis();
-
- if (this.style === Graph3d.STYLE.GRID ||
- this.style === Graph3d.STYLE.SURFACE) {
- this._redrawDataGrid();
- }
- else if (this.style === Graph3d.STYLE.LINE) {
- this._redrawDataLine();
- }
- else if (this.style === Graph3d.STYLE.BAR ||
- this.style === Graph3d.STYLE.BARCOLOR ||
- this.style === Graph3d.STYLE.BARSIZE) {
- this._redrawDataBar();
- }
- else {
- // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
- this._redrawDataDot();
- }
+ DataStep.prototype.snap = function(date) {
- this._redrawInfo();
- this._redrawLegend();
};
/**
- * Clear the canvas before redrawing
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
*/
- Graph3d.prototype._redrawClear = function() {
- var canvas = this.frame.canvas;
- var ctx = canvas.getContext('2d');
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
+ DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
};
+ module.exports = DataStep;
- /**
- * Redraw the legend showing the colors
- */
- Graph3d.prototype._redrawLegend = function() {
- var y;
- if (this.style === Graph3d.STYLE.DOTCOLOR ||
- this.style === Graph3d.STYLE.DOTSIZE) {
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
- var dotSize = this.frame.clientWidth * 0.02;
+ var Emitter = __webpack_require__(41);
+ var Hammer = __webpack_require__(50);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(3);
+ var DataView = __webpack_require__(4);
+ var Range = __webpack_require__(8);
+ var TimeAxis = __webpack_require__(21);
+ var CurrentTime = __webpack_require__(13);
+ var CustomTime = __webpack_require__(14);
+ var LineGraph = __webpack_require__(20);
- var widthMin, widthMax;
- if (this.style === Graph3d.STYLE.DOTSIZE) {
- widthMin = dotSize / 2; // px
- widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
- }
- else {
- widthMin = 20; // px
- widthMax = 20; // px
- }
+ /**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Graph2d.setOptions for the available options.
+ * @constructor
+ */
+ function Graph2d (container, items, options, groups) {
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
- var height = Math.max(this.frame.clientHeight * 0.25, 100);
- var top = this.margin;
- var right = this.frame.clientWidth - this.margin;
- var left = right - widthMax;
- var bottom = top + height;
- }
+ autoResize: true,
- var canvas = this.frame.canvas;
- var ctx = canvas.getContext('2d');
- ctx.lineWidth = 1;
- ctx.font = '14px arial'; // TODO: put in options
+ orientation: 'bottom',
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
- if (this.style === Graph3d.STYLE.DOTCOLOR) {
- // draw the color bar
- var ymin = 0;
- var ymax = height; // Todo: make height customizable
- for (y = ymin; y < ymax; y++) {
- var f = (y - ymin) / (ymax - ymin);
+ // Create the DOM, props, and emitter
+ this._create(container);
- //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
- var hue = f * 240;
- var color = this._hsv2rgb(hue, 1, 1);
+ // all components listed here will be repainted automatically
+ this.components = [];
- ctx.strokeStyle = color;
- ctx.beginPath();
- ctx.moveTo(left, top + y);
- ctx.lineTo(right, top + y);
- ctx.stroke();
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
}
+ };
- ctx.strokeStyle = this.colorAxis;
- ctx.strokeRect(left, top, widthMax, height);
- }
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
- if (this.style === Graph3d.STYLE.DOTSIZE) {
- // draw border around color bar
- ctx.strokeStyle = this.colorAxis;
- ctx.fillStyle = this.colorDot;
- ctx.beginPath();
- ctx.moveTo(left, top);
- ctx.lineTo(right, top);
- ctx.lineTo(right - widthMax + widthMin, bottom);
- ctx.lineTo(left, bottom);
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
- }
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
- if (this.style === Graph3d.STYLE.DOTCOLOR ||
- this.style === Graph3d.STYLE.DOTSIZE) {
- // print values along the color bar
- var gridLineLen = 5; // px
- var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
- step.start();
- if (step.getCurrent() < this.valueMin) {
- step.next();
- }
- while (!step.end()) {
- y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
- ctx.beginPath();
- ctx.moveTo(left - gridLineLen, y);
- ctx.lineTo(left, y);
- ctx.stroke();
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
+ // item set
+ this.linegraph = new LineGraph(this.body);
+ this.components.push(this.linegraph);
- step.next();
- }
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- ctx.textAlign = 'right';
- ctx.textBaseline = 'top';
- var label = this.legendLabel;
- ctx.fillText(label, right, bottom + this.margin);
+ // apply options
+ if (options) {
+ this.setOptions(options);
}
- };
-
- /**
- * Redraw the filter
- */
- Graph3d.prototype._redrawFilter = function() {
- this.frame.filter.innerHTML = '';
-
- if (this.dataFilter) {
- var options = {
- 'visible': this.showAnimationControls
- };
- var slider = new Slider(this.frame.filter, options);
- this.frame.filter.slider = slider;
-
- // TODO: css here is not nice here...
- this.frame.filter.style.padding = '10px';
- //this.frame.filter.style.backgroundColor = '#EFEFEF';
-
- slider.setValues(this.dataFilter.values);
- slider.setPlayInterval(this.animationInterval);
-
- // create an event handler
- var me = this;
- var onchange = function () {
- var index = slider.getIndex();
- me.dataFilter.selectValue(index);
- me.dataPoints = me.dataFilter._getDataPoints();
+ // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+ if (groups) {
+ this.setGroups(groups);
+ }
- me.redraw();
- };
- slider.setOnChangeCallback(onchange);
+ // create itemset
+ if (items) {
+ this.setItems(items);
}
else {
- this.frame.filter.slider = undefined;
- }
- };
-
- /**
- * Redraw the slider
- */
- Graph3d.prototype._redrawSlider = function() {
- if ( this.frame.filter.slider !== undefined) {
- this.frame.filter.slider.redraw();
+ this.redraw();
}
- };
+ }
+ // turn Graph2d into an event emitter
+ Emitter(Graph2d.prototype);
/**
- * Redraw common information
+ * Create the main DOM for the Graph2d: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Graph2d will
+ * be attached.
+ * @private
*/
- Graph3d.prototype._redrawInfo = function() {
- if (this.dataFilter) {
- var canvas = this.frame.canvas;
- var ctx = canvas.getContext('2d');
+ Graph2d.prototype._create = function (container) {
+ this.dom = {};
- ctx.font = '14px arial'; // TODO: put in options
- ctx.lineStyle = 'gray';
- ctx.fillStyle = 'gray';
- ctx.textAlign = 'left';
- ctx.textBaseline = 'top';
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontalContainer = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
- var x = this.margin;
- var y = this.margin;
- ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
- }
- };
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
- /**
- * Redraw the axis
- */
- Graph3d.prototype._redrawAxis = function() {
- var canvas = this.frame.canvas,
- ctx = canvas.getContext('2d'),
- from, to, step, prettyStep,
- text, xText, yText, zText,
- offset, xOffset, yOffset,
- xMin2d, xMax2d;
+ this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
- // TODO: get the actual rendered style of the containerElement
- //ctx.font = this.containerElement.style.font;
- ctx.font = 24 / this.camera.getArmLength() + 'px arial';
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
- // calculate the length for the short grid lines
- var gridLenX = 0.025 / this.scale.x;
- var gridLenY = 0.025 / this.scale.y;
- var textMargin = 5 / this.camera.getArmLength(); // px
- var armAngle = this.camera.getArmRotation().horizontal;
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('change', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.bind(this));
- // draw x-grid lines
- ctx.lineWidth = 1;
- prettyStep = (this.defaultXStep === undefined);
- step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
- step.start();
- if (step.getCurrent() < this.xMin) {
- step.next();
- }
- while (!step.end()) {
- var x = step.getCurrent();
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.dom.root, {
+ prevent_default: true
+ });
+ this.listeners = {};
- if (this.showGrid) {
- from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
- to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
- ctx.strokeStyle = this.colorGrid;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
- }
- else {
- from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
- to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
+ ];
+ events.forEach(function (event) {
+ var listener = function () {
+ var args = [event].concat(Array.prototype.slice.call(arguments, 0));
+ me.emit.apply(me, args);
+ };
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
- from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
- to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
- }
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
- yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
- text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
- if (Math.cos(armAngle * 2) > 0) {
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- text.y += textMargin;
- }
- else if (Math.sin(armAngle * 2) < 0){
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- }
- else {
- ctx.textAlign = 'left';
- ctx.textBaseline = 'middle';
- }
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
+ };
- step.next();
- }
+ /**
+ * Destroy the Graph2d, clean up all DOM elements and event listeners.
+ */
+ Graph2d.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
- // draw y-grid lines
- ctx.lineWidth = 1;
- prettyStep = (this.defaultYStep === undefined);
- step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
- step.start();
- if (step.getCurrent() < this.yMin) {
- step.next();
+ // remove all event listeners
+ this.off();
+
+ // stop checking for changed size
+ this._stopAutoResize();
+
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
}
- while (!step.end()) {
- if (this.showGrid) {
- from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
- to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
- ctx.strokeStyle = this.colorGrid;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
- }
- else {
- from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
- to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
+ this.dom = null;
- from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
- to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
}
+ }
+ this.listeners = null;
+ this.hammer = null;
- xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
- text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
- if (Math.cos(armAngle * 2) < 0) {
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- text.y += textMargin;
- }
- else if (Math.sin(armAngle * 2) > 0){
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- }
- else {
- ctx.textAlign = 'left';
- ctx.textBaseline = 'middle';
- }
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
- step.next();
+ this.body = null;
+ };
+
+ /**
+ * Set options. Options will be passed to all components loaded in the Graph2d.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Graph2d,
+ * can be 'bottom' (default) or 'top'.
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Graph2d will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
+ */
+ Graph2d.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
+ util.selectiveExtend(fields, this.options, options);
+
+ // enable/disable autoResize
+ this._initAutoResize();
}
- // draw z-grid lines and axis
- ctx.lineWidth = 1;
- prettyStep = (this.defaultZStep === undefined);
- step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
- step.start();
- if (step.getCurrent() < this.zMin) {
- step.next();
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
+
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
+ if (options && options.order) {
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
- xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
- yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
- while (!step.end()) {
- // TODO: make z-grid lines really 3d?
- from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(from.x - textMargin, from.y);
- ctx.stroke();
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
+ // redraw everything
+ this.redraw();
+ };
- step.next();
+ /**
+ * Set a custom time bar
+ * @param {Date} time
+ */
+ Graph2d.prototype.setCustomTime = function (time) {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- ctx.lineWidth = 1;
- from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
- to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
- // draw x-axis
- ctx.lineWidth = 1;
- // line at yMin
- xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
- xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(xMin2d.x, xMin2d.y);
- ctx.lineTo(xMax2d.x, xMax2d.y);
- ctx.stroke();
- // line at ymax
- xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
- xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(xMin2d.x, xMin2d.y);
- ctx.lineTo(xMax2d.x, xMax2d.y);
- ctx.stroke();
+ this.customTime.setCustomTime(time);
+ };
- // draw y-axis
- ctx.lineWidth = 1;
- // line at xMin
- from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
- to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
- // line at xMax
- from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
- to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
- ctx.strokeStyle = this.colorAxis;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(to.x, to.y);
- ctx.stroke();
-
- // draw x-label
- var xLabel = this.xLabel;
- if (xLabel.length > 0) {
- yOffset = 0.1 / this.scale.y;
- xText = (this.xMin + this.xMax) / 2;
- yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
- text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
- if (Math.cos(armAngle * 2) > 0) {
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- }
- else if (Math.sin(armAngle * 2) < 0){
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- }
- else {
- ctx.textAlign = 'left';
- ctx.textBaseline = 'middle';
- }
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(xLabel, text.x, text.y);
- }
-
- // draw y-label
- var yLabel = this.yLabel;
- if (yLabel.length > 0) {
- xOffset = 0.1 / this.scale.x;
- xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
- yText = (this.yMin + this.yMax) / 2;
- text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
- if (Math.cos(armAngle * 2) < 0) {
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- }
- else if (Math.sin(armAngle * 2) > 0){
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- }
- else {
- ctx.textAlign = 'left';
- ctx.textBaseline = 'middle';
- }
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(yLabel, text.x, text.y);
+ /**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+ Graph2d.prototype.getCustomTime = function() {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- // draw z-label
- var zLabel = this.zLabel;
- if (zLabel.length > 0) {
- offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
- xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
- yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
- zText = (this.zMin + this.zMax) / 2;
- text = this._convert3Dto2D(new Point3d(xText, yText, zText));
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = this.colorAxis;
- ctx.fillText(zLabel, text.x - offset, text.y);
- }
+ return this.customTime.getCustomTime();
};
/**
- * Calculate the color based on the given value.
- * @param {Number} H Hue, a value be between 0 and 360
- * @param {Number} S Saturation, a value between 0 and 1
- * @param {Number} V Value, a value between 0 and 1
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
- Graph3d.prototype._hsv2rgb = function(H, S, V) {
- var R, G, B, C, Hi, X;
+ Graph2d.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
- C = V * S;
- Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
- X = C * (1 - Math.abs(((H/60) % 2) - 1));
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ }
- switch (Hi) {
- case 0: R = C; G = X; B = 0; break;
- case 1: R = X; G = C; B = 0; break;
- case 2: R = 0; G = C; B = X; break;
- case 3: R = 0; G = X; B = C; break;
- case 4: R = X; G = 0; B = C; break;
- case 5: R = C; G = 0; B = X; break;
+ // set items
+ this.itemsData = newDataSet;
+ this.linegraph && this.linegraph.setItems(newDataSet);
- default: R = 0; G = 0; B = 0; break;
- }
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
+ this.fit();
- return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
- };
+ var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
+ var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
+ this.setWindow(start, end);
+ }
+ };
/**
- * Draw all datapoints as a grid
- * This function can be used when the style is 'grid'
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
- Graph3d.prototype._redrawDataGrid = function() {
- var canvas = this.frame.canvas,
- ctx = canvas.getContext('2d'),
- point, right, top, cross,
- i,
- topSideVisible, fillStyle, strokeStyle, lineWidth,
- h, s, v, zAvg;
-
-
- if (this.dataPoints === undefined || this.dataPoints.length <= 0)
- return; // TODO: throw exception?
+ Graph2d.prototype.setGroups = function(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
- // calculate the translations and screen position of all points
- for (i = 0; i < this.dataPoints.length; i++) {
- var trans = this._convertPointToTranslation(this.dataPoints[i].point);
- var screen = this._convertTranslationToScreen(trans);
+ this.groupsData = newDataSet;
+ this.linegraph.setGroups(newDataSet);
+ };
- this.dataPoints[i].trans = trans;
- this.dataPoints[i].screen = screen;
+ /**
+ * Clear the Graph2d. By Default, items, groups and options are cleared.
+ * Example usage:
+ *
+ * timeline.clear(); // clear items, groups, and options
+ * timeline.clear({options: true}); // clear options only
+ *
+ * @param {Object} [what] Optionally specify what to clear. By default:
+ * {items: true, groups: true, options: true}
+ */
+ Graph2d.prototype.clear = function(what) {
+ // clear items
+ if (!what || what.items) {
+ this.setItems(null);
+ }
- // calculate the translation of the point at the bottom (needed for sorting)
- var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
- this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ // clear groups
+ if (!what || what.groups) {
+ this.setGroups(null);
}
- // sort the points on depth of their (x,y) position (not on z)
- var sortDepth = function (a, b) {
- return b.dist - a.dist;
- };
- this.dataPoints.sort(sortDepth);
+ // clear options of timeline and of each of the components
+ if (!what || what.options) {
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ });
- if (this.style === Graph3d.STYLE.SURFACE) {
- for (i = 0; i < this.dataPoints.length; i++) {
- point = this.dataPoints[i];
- right = this.dataPoints[i].pointRight;
- top = this.dataPoints[i].pointTop;
- cross = this.dataPoints[i].pointCross;
+ this.setOptions(this.defaultOptions); // this will also do a redraw
+ }
+ };
- if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
+ /**
+ * Set Graph2d window such that it fits all items
+ */
+ Graph2d.prototype.fit = function() {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
- if (this.showGrayBottom || this.showShadow) {
- // calculate the cross product of the two vectors from center
- // to left and right, in order to know whether we are looking at the
- // bottom or at the top side. We can also use the cross product
- // for calculating light intensity
- var aDiff = Point3d.subtract(cross.trans, point.trans);
- var bDiff = Point3d.subtract(top.trans, right.trans);
- var crossproduct = Point3d.crossProduct(aDiff, bDiff);
- var len = crossproduct.length();
- // FIXME: there is a bug with determining the surface side (shadow or colored)
+ // add 5% space on both sides
+ var start = dataRange.min;
+ var end = dataRange.max;
+ if (start != null && end != null) {
+ var interval = (end.valueOf() - start.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ start = new Date(start.valueOf() - interval * 0.05);
+ end = new Date(end.valueOf() + interval * 0.05);
+ }
- topSideVisible = (crossproduct.z > 0);
- }
- else {
- topSideVisible = true;
- }
+ // skip range set if there is no start and end date
+ if (start === null && end === null) {
+ return;
+ }
- if (topSideVisible) {
- // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
- zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
- h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
- s = 1; // saturation
+ this.range.setRange(start, end);
+ };
- if (this.showShadow) {
- v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
- fillStyle = this._hsv2rgb(h, s, v);
- strokeStyle = fillStyle;
- }
- else {
- v = 1;
- fillStyle = this._hsv2rgb(h, s, v);
- strokeStyle = this.colorAxis;
- }
- }
- else {
- fillStyle = 'gray';
- strokeStyle = this.colorAxis;
- }
- lineWidth = 0.5;
-
- ctx.lineWidth = lineWidth;
- ctx.fillStyle = fillStyle;
- ctx.strokeStyle = strokeStyle;
- ctx.beginPath();
- ctx.moveTo(point.screen.x, point.screen.y);
- ctx.lineTo(right.screen.x, right.screen.y);
- ctx.lineTo(cross.screen.x, cross.screen.y);
- ctx.lineTo(top.screen.x, top.screen.y);
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
- }
- }
- }
- else { // grid style
- for (i = 0; i < this.dataPoints.length; i++) {
- point = this.dataPoints[i];
- right = this.dataPoints[i].pointRight;
- top = this.dataPoints[i].pointTop;
-
- if (point !== undefined) {
- if (this.showPerspective) {
- lineWidth = 2 / -point.trans.z;
- }
- else {
- lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
- }
- }
+ /**
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
+ */
+ Graph2d.prototype.getItemRange = function() {
+ // calculate min from start filed
+ var itemsData = this.itemsData,
+ min = null,
+ max = null;
- if (point !== undefined && right !== undefined) {
- // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
- zAvg = (point.point.z + right.point.z) / 2;
- h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ if (itemsData) {
+ // calculate the minimum value of the field 'start'
+ var minItem = itemsData.min('start');
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
- ctx.beginPath();
- ctx.moveTo(point.screen.x, point.screen.y);
- ctx.lineTo(right.screen.x, right.screen.y);
- ctx.stroke();
+ // calculate maximum value of fields 'start' and 'end'
+ var maxStartItem = itemsData.max('start');
+ if (maxStartItem) {
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
+ }
+ var maxEndItem = itemsData.max('end');
+ if (maxEndItem) {
+ if (max == null) {
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
}
-
- if (point !== undefined && top !== undefined) {
- // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
- zAvg = (point.point.z + top.point.z) / 2;
- h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
-
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
- ctx.beginPath();
- ctx.moveTo(point.screen.x, point.screen.y);
- ctx.lineTo(top.screen.x, top.screen.y);
- ctx.stroke();
+ else {
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
}
}
}
- };
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
+ };
/**
- * Draw all datapoints as dots.
- * This function can be used when the style is 'dot' or 'dot-line'
+ * Set the visible window. Both parameters are optional, you can change only
+ * start or only end. Syntax:
+ *
+ * TimeLine.setWindow(start, end)
+ * TimeLine.setWindow(range)
+ *
+ * Where start and end can be a Date, number, or string, and range is an
+ * object with properties start and end.
+ *
+ * @param {Date | Number | String | Object} [start] Start date of visible window
+ * @param {Date | Number | String} [end] End date of visible window
*/
- Graph3d.prototype._redrawDataDot = function() {
- var canvas = this.frame.canvas;
- var ctx = canvas.getContext('2d');
- var i;
-
- if (this.dataPoints === undefined || this.dataPoints.length <= 0)
- return; // TODO: throw exception?
-
- // calculate the translations of all points
- for (i = 0; i < this.dataPoints.length; i++) {
- var trans = this._convertPointToTranslation(this.dataPoints[i].point);
- var screen = this._convertTranslationToScreen(trans);
- this.dataPoints[i].trans = trans;
- this.dataPoints[i].screen = screen;
-
- // calculate the distance from the point at the bottom to the camera
- var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
- this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ Graph2d.prototype.setWindow = function(start, end) {
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end);
}
-
- // order the translated points by depth
- var sortDepth = function (a, b) {
- return b.dist - a.dist;
- };
- this.dataPoints.sort(sortDepth);
-
- // draw the datapoints as colored circles
- var dotSize = this.frame.clientWidth * 0.02; // px
- for (i = 0; i < this.dataPoints.length; i++) {
- var point = this.dataPoints[i];
-
- if (this.style === Graph3d.STYLE.DOTLINE) {
- // draw a vertical line from the bottom to the graph value
- //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
- var from = this._convert3Dto2D(point.bottom);
- ctx.lineWidth = 1;
- ctx.strokeStyle = this.colorGrid;
- ctx.beginPath();
- ctx.moveTo(from.x, from.y);
- ctx.lineTo(point.screen.x, point.screen.y);
- ctx.stroke();
- }
-
- // calculate radius for the circle
- var size;
- if (this.style === Graph3d.STYLE.DOTSIZE) {
- size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
- }
- else {
- size = dotSize;
- }
-
- var radius;
- if (this.showPerspective) {
- radius = size / -point.trans.z;
- }
- else {
- radius = size * -(this.eye.z / this.camera.getArmLength());
- }
- if (radius < 0) {
- radius = 0;
- }
-
- var hue, color, borderColor;
- if (this.style === Graph3d.STYLE.DOTCOLOR ) {
- // calculate the color based on the value
- hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
- color = this._hsv2rgb(hue, 1, 1);
- borderColor = this._hsv2rgb(hue, 1, 0.8);
- }
- else if (this.style === Graph3d.STYLE.DOTSIZE) {
- color = this.colorDot;
- borderColor = this.colorDotBorder;
- }
- else {
- // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
- hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
- color = this._hsv2rgb(hue, 1, 1);
- borderColor = this._hsv2rgb(hue, 1, 0.8);
- }
-
- // draw the circle
- ctx.lineWidth = 1.0;
- ctx.strokeStyle = borderColor;
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
- ctx.fill();
- ctx.stroke();
+ else {
+ this.range.setRange(start, end);
}
};
/**
- * Draw all datapoints as bars.
- * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
+ * Get the visible window
+ * @return {{start: Date, end: Date}} Visible range
*/
- Graph3d.prototype._redrawDataBar = function() {
- var canvas = this.frame.canvas;
- var ctx = canvas.getContext('2d');
- var i, j, surface, corners;
+ Graph2d.prototype.getWindow = function() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
+ };
- if (this.dataPoints === undefined || this.dataPoints.length <= 0)
- return; // TODO: throw exception?
+ /**
+ * Force a redraw of the Graph2d. Can be useful to manually redraw when
+ * option autoResize=false
+ */
+ Graph2d.prototype.redraw = function() {
+ var resized = false,
+ options = this.options,
+ props = this.props,
+ dom = this.dom;
- // calculate the translations of all points
- for (i = 0; i < this.dataPoints.length; i++) {
- var trans = this._convertPointToTranslation(this.dataPoints[i].point);
- var screen = this._convertTranslationToScreen(trans);
- this.dataPoints[i].trans = trans;
- this.dataPoints[i].screen = screen;
+ if (!dom) return; // when destroyed
- // calculate the distance from the point at the bottom to the camera
- var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
- this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
- }
+ // update class names
+ dom.root.className = 'vis timeline root ' + options.orientation;
- // order the translated points by depth
- var sortDepth = function (a, b) {
- return b.dist - a.dist;
- };
- this.dataPoints.sort(sortDepth);
+ // update root width and height options
+ dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
+ dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
- // draw the datapoints as bars
- var xWidth = this.xBarWidth / 2;
- var yWidth = this.yBarWidth / 2;
- for (i = 0; i < this.dataPoints.length; i++) {
- var point = this.dataPoints[i];
-
- // determine color
- var hue, color, borderColor;
- if (this.style === Graph3d.STYLE.BARCOLOR ) {
- // calculate the color based on the value
- hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
- color = this._hsv2rgb(hue, 1, 1);
- borderColor = this._hsv2rgb(hue, 1, 0.8);
- }
- else if (this.style === Graph3d.STYLE.BARSIZE) {
- color = this.colorDot;
- borderColor = this.colorDotBorder;
- }
- else {
- // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
- hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
- color = this._hsv2rgb(hue, 1, 1);
- borderColor = this._hsv2rgb(hue, 1, 0.8);
- }
-
- // calculate size for the bar
- if (this.style === Graph3d.STYLE.BARSIZE) {
- xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
- yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
- }
-
- // calculate all corner points
- var me = this;
- var point3d = point.point;
- var top = [
- {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
- {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
- {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
- {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
- ];
- var bottom = [
- {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
- {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
- {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
- {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
- ];
-
- // calculate screen location of the points
- top.forEach(function (obj) {
- obj.screen = me._convert3Dto2D(obj.point);
- });
- bottom.forEach(function (obj) {
- obj.screen = me._convert3Dto2D(obj.point);
- });
-
- // create five sides, calculate both corner points and center points
- var surfaces = [
- {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
- {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
- {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
- {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
- {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
- ];
- point.surfaces = surfaces;
-
- // calculate the distance of each of the surface centers to the camera
- for (j = 0; j < surfaces.length; j++) {
- surface = surfaces[j];
- var transCenter = this._convertPointToTranslation(surface.center);
- surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
- // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
- // but the current solution is fast/simple and works in 99.9% of all cases
- // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
- }
-
- // order the surfaces by their (translated) depth
- surfaces.sort(function (a, b) {
- var diff = b.dist - a.dist;
- if (diff) return diff;
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
- // if equal depth, sort the top surface last
- if (a.corners === top) return 1;
- if (b.corners === top) return -1;
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
- // both are equal
- return 0;
- });
+ // TODO: compensate borders when any of the panels is empty.
- // draw the ordered surfaces
- ctx.lineWidth = 1;
- ctx.strokeStyle = borderColor;
- ctx.fillStyle = color;
- // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
- for (j = 2; j < surfaces.length; j++) {
- surface = surfaces[j];
- corners = surface.corners;
- ctx.beginPath();
- ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
- ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
- ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
- ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
- ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
- ctx.fill();
- ctx.stroke();
- }
- }
- };
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
- /**
- * Draw a line through all datapoints.
- * This function can be used when the style is 'line'
- */
- Graph3d.prototype._redrawDataLine = function() {
- var canvas = this.frame.canvas,
- ctx = canvas.getContext('2d'),
- point, i;
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
- if (this.dataPoints === undefined || this.dataPoints.length <= 0)
- return; // TODO: throw exception?
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
- // calculate the translations of all points
- for (i = 0; i < this.dataPoints.length; i++) {
- var trans = this._convertPointToTranslation(this.dataPoints[i].point);
- var screen = this._convertTranslationToScreen(trans);
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
- this.dataPoints[i].trans = trans;
- this.dataPoints[i].screen = screen;
- }
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = props.left.width + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontalContainer.style.left = '0';
+ dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
- // start the line
- if (this.dataPoints.length > 0) {
- point = this.dataPoints[0];
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Graph2d or of the contents of the center changed
+ this._updateScrollTop();
- ctx.lineWidth = 1; // TODO: make customizable
- ctx.strokeStyle = 'blue'; // TODO: make customizable
- ctx.beginPath();
- ctx.moveTo(point.screen.x, point.screen.y);
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height -
+ this.props.border.top - this.props.border.bottom, 0);
}
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
- // draw the datapoints as colored circles
- for (i = 1; i < this.dataPoints.length; i++) {
- point = this.dataPoints[i];
- ctx.lineTo(point.screen.x, point.screen.y);
- }
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
- // finish the line
- if (this.dataPoints.length > 0) {
- ctx.stroke();
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep redrawing until all sizes are settled
+ this.redraw();
}
};
/**
- * Start a moving operation inside the provided parent element
- * @param {Event} event The event that occurred (required for
- * retrieving the mouse position)
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
*/
- Graph3d.prototype._onMouseDown = function(event) {
- event = event || window.event;
+ // TODO: move this function to Range
+ Graph2d.prototype._toTime = function(x) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return new Date(x / conversion.scale + conversion.offset);
+ };
- // check if mouse is still down (may be up when focus is lost for example
- // in an iframe)
- if (this.leftButtonDown) {
- this._onMouseUp(event);
- }
-
- // only react on left mouse button down
- this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
- if (!this.leftButtonDown && !this.touchDown) return;
-
- // get mouse position (different code for IE and all other browsers)
- this.startMouseX = getMouseX(event);
- this.startMouseY = getMouseY(event);
-
- this.startStart = new Date(this.start);
- this.startEnd = new Date(this.end);
- this.startArmRotation = this.camera.getArmRotation();
-
- this.frame.style.cursor = 'move';
-
- // add event listeners to handle moving the contents
- // we store the function onmousemove and onmouseup in the graph, so we can
- // remove the eventlisteners lateron in the function mouseUp()
- var me = this;
- this.onmousemove = function (event) {me._onMouseMove(event);};
- this.onmouseup = function (event) {me._onMouseUp(event);};
- G3DaddEventListener(document, 'mousemove', me.onmousemove);
- G3DaddEventListener(document, 'mouseup', me.onmouseup);
- G3DpreventDefault(event);
+ /**
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+ // TODO: move this function to Range
+ Graph2d.prototype._toGlobalTime = function(x) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return new Date(x / conversion.scale + conversion.offset);
};
-
/**
- * Perform moving operating.
- * This function activated from within the funcion Graph.mouseDown().
- * @param {Event} event Well, eehh, the event
+ * Convert a datetime (Date object) into a position on the screen
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ * @private
*/
- Graph3d.prototype._onMouseMove = function (event) {
- event = event || window.event;
-
- // calculate change in mouse position
- var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
- var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
-
- var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
- var verticalNew = this.startArmRotation.vertical + diffY / 200;
-
- var snapAngle = 4; // degrees
- var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
-
- // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
- // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
- if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
- horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
- }
- if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
- horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
- }
-
- // snap vertically to nice angles
- if (Math.abs(Math.sin(verticalNew)) < snapValue) {
- verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
- }
- if (Math.abs(Math.cos(verticalNew)) < snapValue) {
- verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
- }
-
- this.camera.setArmRotation(horizontalNew, verticalNew);
- this.redraw();
-
- // fire a cameraPositionChange event
- var parameters = this.getCameraPosition();
- this.emit('cameraPositionChange', parameters);
-
- G3DpreventDefault(event);
+ // TODO: move this function to Range
+ Graph2d.prototype._toScreen = function(time) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
};
/**
- * Stop moving operating.
- * This function activated from within the funcion Graph.mouseDown().
- * @param {event} event The event
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
*/
- Graph3d.prototype._onMouseUp = function (event) {
- this.frame.style.cursor = 'auto';
- this.leftButtonDown = false;
+ // TODO: move this function to Range
+ Graph2d.prototype._toGlobalScreen = function(time) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+ };
- // remove event listeners here
- G3DremoveEventListener(document, 'mousemove', this.onmousemove);
- G3DremoveEventListener(document, 'mouseup', this.onmouseup);
- G3DpreventDefault(event);
+ /**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+ Graph2d.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
+ }
+ else {
+ this._stopAutoResize();
+ }
};
/**
- * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
- * @param {Event} event A mouse move event
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
*/
- Graph3d.prototype._onTooltip = function (event) {
- var delay = 300; // ms
- var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
- var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
+ Graph2d.prototype._startAutoResize = function () {
+ var me = this;
- if (!this.showTooltip) {
- return;
- }
+ this._stopAutoResize();
- if (this.tooltipTimeout) {
- clearTimeout(this.tooltipTimeout);
- }
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
+ }
- // (delayed) display of a tooltip only if no mouse button is down
- if (this.leftButtonDown) {
- this._hideTooltip();
- return;
- }
+ if (me.dom.root) {
+ // check whether the frame is resized
+ if ((me.dom.root.clientWidth != me.props.lastWidth) ||
+ (me.dom.root.clientHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.clientWidth;
+ me.props.lastHeight = me.dom.root.clientHeight;
- if (this.tooltip && this.tooltip.dataPoint) {
- // tooltip is currently visible
- var dataPoint = this._dataPointFromXY(mouseX, mouseY);
- if (dataPoint !== this.tooltip.dataPoint) {
- // datapoint changed
- if (dataPoint) {
- this._showTooltip(dataPoint);
- }
- else {
- this._hideTooltip();
+ me.emit('change');
}
}
- }
- else {
- // tooltip is currently not visible
- var me = this;
- this.tooltipTimeout = setTimeout(function () {
- me.tooltipTimeout = null;
+ };
- // show a tooltip if we have a data point
- var dataPoint = me._dataPointFromXY(mouseX, mouseY);
- if (dataPoint) {
- me._showTooltip(dataPoint);
- }
- }, delay);
- }
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
+
+ this.watchTimer = setInterval(this._onResize, 1000);
};
/**
- * Event handler for touchstart event on mobile devices
+ * Stop watching for a resize of the frame.
+ * @private
*/
- Graph3d.prototype._onTouchStart = function(event) {
- this.touchDown = true;
-
- var me = this;
- this.ontouchmove = function (event) {me._onTouchMove(event);};
- this.ontouchend = function (event) {me._onTouchEnd(event);};
- G3DaddEventListener(document, 'touchmove', me.ontouchmove);
- G3DaddEventListener(document, 'touchend', me.ontouchend);
+ Graph2d.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
- this._onMouseDown(event);
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
};
/**
- * Event handler for touchmove event on mobile devices
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
*/
- Graph3d.prototype._onTouchMove = function(event) {
- this._onMouseMove(event);
+ Graph2d.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
};
/**
- * Event handler for touchend event on mobile devices
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
*/
- Graph3d.prototype._onTouchEnd = function(event) {
- this.touchDown = false;
-
- G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
- G3DremoveEventListener(document, 'touchend', this.ontouchend);
-
- this._onMouseUp(event);
+ Graph2d.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
};
+ /**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+ Graph2d.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+ };
/**
- * Event handler for mouse wheel event, used to zoom the graph
- * Code from http://adomas.org/javascript-mouse-wheel/
- * @param {event} event The event
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
*/
- Graph3d.prototype._onWheel = function(event) {
- if (!event) /* For IE. */
- event = window.event;
+ Graph2d.prototype._onDrag = function (event) {
+ // 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
+ if (!this.touch.allowDragging) return;
- // retrieve delta
- var delta = 0;
- if (event.wheelDelta) { /* IE/Opera. */
- delta = event.wheelDelta/120;
- } else if (event.detail) { /* Mozilla case. */
- // In Mozilla, sign of delta is different than in IE.
- // Also, delta is multiple of 3.
- delta = -event.detail/3;
- }
-
- // If delta is nonzero, handle it.
- // Basically, delta is now positive if wheel was scrolled up,
- // and negative, if wheel was scrolled down.
- if (delta) {
- var oldLength = this.camera.getArmLength();
- var newLength = oldLength * (1 - delta / 10);
+ var delta = event.gesture.deltaY;
- this.camera.setArmLength(newLength);
- this.redraw();
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
- this._hideTooltip();
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
}
-
- // fire a cameraPositionChange event
- var parameters = this.getCameraPosition();
- this.emit('cameraPositionChange', parameters);
-
- // Prevent default actions caused by mouse wheel.
- // That might be ugly, but we handle scrolls somehow
- // anyway, so don't bother here..
- G3DpreventDefault(event);
};
/**
- * Test whether a point lies inside given 2D triangle
- * @param {Point2d} point
- * @param {Point2d[]} triangle
- * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
- Graph3d.prototype._insideTriangle = function (point, triangle) {
- var a = triangle[0],
- b = triangle[1],
- c = triangle[2];
+ Graph2d.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
+ };
- function sign (x) {
- return x > 0 ? 1 : x < 0 ? -1 : 0;
+ /**
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+ Graph2d.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ }
+ this.props.scrollTopMin = scrollTopMin;
}
- var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
- var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
- var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
- // each of the three signs must be either equal to each other or zero
- return (as == 0 || bs == 0 || as == bs) &&
- (bs == 0 || cs == 0 || bs == cs) &&
- (as == 0 || cs == 0 || as == cs);
+ return this.props.scrollTop;
};
/**
- * Find a data point close to given screen position (x, y)
- * @param {Number} x
- * @param {Number} y
- * @return {Object | null} The closest data point or null if not close to any data point
+ * Get the current scrollTop
+ * @returns {number} scrollTop
* @private
*/
- Graph3d.prototype._dataPointFromXY = function (x, y) {
- var i,
- distMax = 100, // px
- dataPoint = null,
- closestDataPoint = null,
- closestDist = null,
- center = new Point2d(x, y);
+ Graph2d.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
+ };
- if (this.style === Graph3d.STYLE.BAR ||
- this.style === Graph3d.STYLE.BARCOLOR ||
- this.style === Graph3d.STYLE.BARSIZE) {
- // the data points are ordered from far away to closest
- for (i = this.dataPoints.length - 1; i >= 0; i--) {
- dataPoint = this.dataPoints[i];
- var surfaces = dataPoint.surfaces;
- if (surfaces) {
- for (var s = surfaces.length - 1; s >= 0; s--) {
- // split each surface in two triangles, and see if the center point is inside one of these
- var surface = surfaces[s];
- var corners = surface.corners;
- var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
- var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
- if (this._insideTriangle(center, triangle1) ||
- this._insideTriangle(center, triangle2)) {
- // return immediately at the first hit
- return dataPoint;
- }
- }
- }
- }
- }
- else {
- // find the closest data point, using distance to the center of the point on 2d screen
- for (i = 0; i < this.dataPoints.length; i++) {
- dataPoint = this.dataPoints[i];
- var point = dataPoint.screen;
- if (point) {
- var distX = Math.abs(x - point.x);
- var distY = Math.abs(y - point.y);
- var dist = Math.sqrt(distX * distX + distY * distY);
+ module.exports = Graph2d;
- if ((closestDist === null || dist < closestDist) && dist < distMax) {
- closestDist = dist;
- closestDataPoint = dataPoint;
- }
- }
- }
- }
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
- return closestDataPoint;
- };
+ var util = __webpack_require__(1);
+ var moment = __webpack_require__(39);
+ var Component = __webpack_require__(12);
/**
- * Display a tooltip for given data point
- * @param {Object} dataPoint
- * @private
+ * @constructor Range
+ * A Range controls a numeric range with a start and end value.
+ * The Range adjusts the range based on mouse events or programmatic changes,
+ * and triggers events when the range is changing or has been changed.
+ * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
+ * @param {Object} [options] See description at Range.setOptions
*/
- Graph3d.prototype._showTooltip = function (dataPoint) {
- var content, line, dot;
-
- if (!this.tooltip) {
- content = document.createElement('div');
- content.style.position = 'absolute';
- content.style.padding = '10px';
- content.style.border = '1px solid #4d4d4d';
- content.style.color = '#1a1a1a';
- content.style.background = 'rgba(255,255,255,0.7)';
- content.style.borderRadius = '2px';
- content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
+ function Range(body, options) {
+ var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
+ this.start = now.clone().add('days', -3).valueOf(); // Number
+ this.end = now.clone().add('days', 4).valueOf(); // Number
- line = document.createElement('div');
- line.style.position = 'absolute';
- line.style.height = '40px';
- line.style.width = '0';
- line.style.borderLeft = '1px solid #4d4d4d';
+ this.body = body;
- dot = document.createElement('div');
- dot.style.position = 'absolute';
- dot.style.height = '0';
- dot.style.width = '0';
- dot.style.border = '5px solid #4d4d4d';
- dot.style.borderRadius = '5px';
+ // default options
+ this.defaultOptions = {
+ start: null,
+ end: null,
+ direction: 'horizontal', // 'horizontal' or 'vertical'
+ moveable: true,
+ zoomable: true,
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
+ };
+ this.options = util.extend({}, this.defaultOptions);
- this.tooltip = {
- dataPoint: null,
- dom: {
- content: content,
- line: line,
- dot: dot
- }
- };
- }
- else {
- content = this.tooltip.dom.content;
- line = this.tooltip.dom.line;
- dot = this.tooltip.dom.dot;
- }
+ this.props = {
+ touch: {}
+ };
- this._hideTooltip();
+ // drag listeners for dragging
+ this.body.emitter.on('dragstart', this._onDragStart.bind(this));
+ this.body.emitter.on('drag', this._onDrag.bind(this));
+ this.body.emitter.on('dragend', this._onDragEnd.bind(this));
- this.tooltip.dataPoint = dataPoint;
- if (typeof this.showTooltip === 'function') {
- content.innerHTML = this.showTooltip(dataPoint.point);
- }
- else {
- content.innerHTML = '
' +
- 'x: | ' + dataPoint.point.x + ' |
' +
- 'y: | ' + dataPoint.point.y + ' |
' +
- 'z: | ' + dataPoint.point.z + ' |
' +
- '
';
- }
+ // ignore dragging when holding
+ this.body.emitter.on('hold', this._onHold.bind(this));
- content.style.left = '0';
- content.style.top = '0';
- this.frame.appendChild(content);
- this.frame.appendChild(line);
- this.frame.appendChild(dot);
+ // mouse wheel for zooming
+ this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
+ this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
- // calculate sizes
- var contentWidth = content.offsetWidth;
- var contentHeight = content.offsetHeight;
- var lineHeight = line.offsetHeight;
- var dotWidth = dot.offsetWidth;
- var dotHeight = dot.offsetHeight;
+ // pinch to zoom
+ this.body.emitter.on('touch', this._onTouch.bind(this));
+ this.body.emitter.on('pinch', this._onPinch.bind(this));
- var left = dataPoint.screen.x - contentWidth / 2;
- left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
+ this.setOptions(options);
+ }
- line.style.left = dataPoint.screen.x + 'px';
- line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
- content.style.left = left + 'px';
- content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
- dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
- dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
- };
+ Range.prototype = new Component();
/**
- * Hide the tooltip when displayed
- * @private
- */
- Graph3d.prototype._hideTooltip = function () {
- if (this.tooltip) {
- this.tooltip.dataPoint = null;
-
- for (var prop in this.tooltip.dom) {
- if (this.tooltip.dom.hasOwnProperty(prop)) {
- var elem = this.tooltip.dom[prop];
- if (elem && elem.parentNode) {
- elem.parentNode.removeChild(elem);
- }
- }
+ * Set options for the range controller
+ * @param {Object} options Available options:
+ * {Number | Date | String} start Start date for the range
+ * {Number | Date | String} end End date for the range
+ * {Number} min Minimum value for start
+ * {Number} max Maximum value for end
+ * {Number} zoomMin Set a minimum value for
+ * (end - start).
+ * {Number} zoomMax Set a maximum value for
+ * (end - start).
+ * {Boolean} moveable Enable moving of the range
+ * by dragging. True by default
+ * {Boolean} zoomable Enable zooming of the range
+ * by pinching/scrolling. True by default
+ */
+ Range.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the options that we know
+ var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('start' in options || 'end' in options) {
+ // apply a new range. both start and end are optional
+ this.setRange(options.start, options.end);
}
}
};
-
/**
- * Add and event listener. Works for all browsers
- * @param {Element} element An html element
- * @param {string} action The action, for example 'click',
- * without the prefix 'on'
- * @param {function} listener The callback function to be executed
- * @param {boolean} useCapture
+ * Test whether direction has a valid value
+ * @param {String} direction 'horizontal' or 'vertical'
*/
- G3DaddEventListener = function(element, action, listener, useCapture) {
- if (element.addEventListener) {
- if (useCapture === undefined)
- useCapture = false;
-
- if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
- action = 'DOMMouseScroll'; // For Firefox
- }
+ function validateDirection (direction) {
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
+ }
- element.addEventListener(action, listener, useCapture);
- } else {
- element.attachEvent('on' + action, listener); // IE browsers
+ /**
+ * Set a new start and end range
+ * @param {Number} [start]
+ * @param {Number} [end]
+ */
+ Range.prototype.setRange = function(start, end) {
+ var changed = this._applyRange(start, end);
+ if (changed) {
+ var params = {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ };
+ this.body.emitter.emit('rangechange', params);
+ this.body.emitter.emit('rangechanged', params);
}
};
/**
- * Remove an event listener from an element
- * @param {Element} element An html dom element
- * @param {string} action The name of the event, for example 'mousedown'
- * @param {function} listener The listener function
- * @param {boolean} useCapture
+ * Set a new start and end range. This method is the same as setRange, but
+ * does not trigger a range change and range changed event, and it returns
+ * true when the range is changed
+ * @param {Number} [start]
+ * @param {Number} [end]
+ * @return {Boolean} changed
+ * @private
*/
- G3DremoveEventListener = function(element, action, listener, useCapture) {
- if (element.removeEventListener) {
- // non-IE browsers
- if (useCapture === undefined)
- useCapture = false;
+ Range.prototype._applyRange = function(start, end) {
+ var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
+ newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
+ max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
+ min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
+ diff;
- if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
- action = 'DOMMouseScroll'; // For Firefox
+ // check for valid number
+ if (isNaN(newStart) || newStart === null) {
+ throw new Error('Invalid start "' + start + '"');
+ }
+ if (isNaN(newEnd) || newEnd === null) {
+ throw new Error('Invalid end "' + end + '"');
+ }
+
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
+ }
+
+ // prevent start < min
+ if (min !== null) {
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+
+ // prevent end > max
+ if (max != null) {
+ if (newEnd > max) {
+ newEnd = max;
+ }
+ }
}
+ }
- element.removeEventListener(action, listener, useCapture);
- } else {
- // IE browsers
- element.detachEvent('on' + action, listener);
+ // prevent end > max
+ if (max !== null) {
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+
+ // prevent start < min
+ if (min != null) {
+ if (newStart < min) {
+ newStart = min;
+ }
+ }
+ }
+ }
+
+ // prevent (end-start) < zoomMin
+ if (this.options.zoomMin !== null) {
+ var zoomMin = parseFloat(this.options.zoomMin);
+ if (zoomMin < 0) {
+ zoomMin = 0;
+ }
+ if ((newEnd - newStart) < zoomMin) {
+ if ((this.end - this.start) === zoomMin) {
+ // ignore this action, we are already zoomed to the minimum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the minimum
+ diff = (zoomMin - (newEnd - newStart));
+ newStart -= diff / 2;
+ newEnd += diff / 2;
+ }
+ }
}
+
+ // prevent (end-start) > zoomMax
+ if (this.options.zoomMax !== null) {
+ var zoomMax = parseFloat(this.options.zoomMax);
+ if (zoomMax < 0) {
+ zoomMax = 0;
+ }
+ if ((newEnd - newStart) > zoomMax) {
+ if ((this.end - this.start) === zoomMax) {
+ // ignore this action, we are already zoomed to the maximum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the maximum
+ diff = ((newEnd - newStart) - zoomMax);
+ newStart += diff / 2;
+ newEnd -= diff / 2;
+ }
+ }
+ }
+
+ var changed = (this.start != newStart || this.end != newEnd);
+
+ this.start = newStart;
+ this.end = newEnd;
+
+ return changed;
};
/**
- * Stop event propagation
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
*/
- G3DstopPropagation = function(event) {
- if (!event)
- event = window.event;
+ Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
+ };
- if (event.stopPropagation) {
- event.stopPropagation(); // non-IE browsers
+ /**
+ * Calculate the conversion offset and scale for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+ Range.prototype.conversion = function (width) {
+ return Range.conversion(this.start, this.end, width);
+ };
+
+ /**
+ * Static method to calculate the conversion offset and scale for a range,
+ * based on the provided start, end, and width
+ * @param {Number} start
+ * @param {Number} end
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+ Range.conversion = function (start, end, width) {
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ scale: width / (end - start)
+ }
}
else {
- event.cancelBubble = true; // IE browsers
+ return {
+ offset: 0,
+ scale: 1
+ };
}
};
-
/**
- * Cancels the event if it is cancelable, without stopping further propagation of the event.
+ * Start dragging horizontally or vertically
+ * @param {Event} event
+ * @private
*/
- G3DpreventDefault = function (event) {
- if (!event)
- event = window.event;
+ Range.prototype._onDragStart = function(event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
- if (event.preventDefault) {
- event.preventDefault(); // non-IE browsers
- }
- else {
- event.returnValue = false; // IE browsers
+ // 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
+ if (!this.props.touch.allowDragging) return;
+
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
+
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'move';
}
};
/**
- * @constructor Slider
- *
- * An html slider control with start/stop/prev/next buttons
- * @param {Element} container The element where the slider will be created
- * @param {Object} options Available options:
- * {boolean} visible If true (default) the
- * slider is visible.
+ * Perform dragging operation
+ * @param {Event} event
+ * @private
*/
- function Slider(container, options) {
- if (container === undefined) {
- throw 'Error: No container element defined';
- }
- this.container = container;
- this.visible = (options && options.visible != undefined) ? options.visible : true;
-
- if (this.visible) {
- this.frame = document.createElement('DIV');
- //this.frame.style.backgroundColor = '#E5E5E5';
- this.frame.style.width = '100%';
- this.frame.style.position = 'relative';
- this.container.appendChild(this.frame);
+ Range.prototype._onDrag = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+ var direction = this.options.direction;
+ validateDirection(direction);
+ // 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
+ if (!this.props.touch.allowDragging) return;
+ var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
+ interval = (this.props.touch.end - this.props.touch.start),
+ width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
+ diffRange = -delta / width * interval;
+ this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
+ this.body.emitter.emit('rangechange', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+ };
- this.frame.prev = document.createElement('INPUT');
- this.frame.prev.type = 'BUTTON';
- this.frame.prev.value = 'Prev';
- this.frame.appendChild(this.frame.prev);
+ /**
+ * Stop dragging operation
+ * @param {event} event
+ * @private
+ */
+ Range.prototype._onDragEnd = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
- this.frame.play = document.createElement('INPUT');
- this.frame.play.type = 'BUTTON';
- this.frame.play.value = 'Play';
- this.frame.appendChild(this.frame.play);
+ // 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
+ if (!this.props.touch.allowDragging) return;
- this.frame.next = document.createElement('INPUT');
- this.frame.next.type = 'BUTTON';
- this.frame.next.value = 'Next';
- this.frame.appendChild(this.frame.next);
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'auto';
+ }
- this.frame.bar = document.createElement('INPUT');
- this.frame.bar.type = 'BUTTON';
- this.frame.bar.style.position = 'absolute';
- this.frame.bar.style.border = '1px solid red';
- this.frame.bar.style.width = '100px';
- this.frame.bar.style.height = '6px';
- this.frame.bar.style.borderRadius = '2px';
- this.frame.bar.style.MozBorderRadius = '2px';
- this.frame.bar.style.border = '1px solid #7F7F7F';
- this.frame.bar.style.backgroundColor = '#E5E5E5';
- this.frame.appendChild(this.frame.bar);
+ // fire a rangechanged event
+ this.body.emitter.emit('rangechanged', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+ };
- this.frame.slide = document.createElement('INPUT');
- this.frame.slide.type = 'BUTTON';
- this.frame.slide.style.margin = '0px';
- this.frame.slide.value = ' ';
- this.frame.slide.style.position = 'relative';
- this.frame.slide.style.left = '-100px';
- this.frame.appendChild(this.frame.slide);
+ /**
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @private
+ */
+ Range.prototype._onMouseWheel = function(event) {
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
- // create events
- var me = this;
- this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
- this.frame.prev.onclick = function (event) {me.prev(event);};
- this.frame.play.onclick = function (event) {me.togglePlay(event);};
- this.frame.next.onclick = function (event) {me.next(event);};
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta / 120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail / 3;
}
- this.onChangeCallback = undefined;
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ // perform the zoom action. Delta is normally 1 or -1
- this.values = [];
- this.index = undefined;
+ // adjust a negative delta such that zooming in with delta 0.1
+ // equals zooming out with a delta -0.1
+ var scale;
+ if (delta < 0) {
+ scale = 1 - (delta / 5);
+ }
+ else {
+ scale = 1 / (1 + (delta / 5)) ;
+ }
- this.playTimeout = undefined;
- this.playInterval = 1000; // milliseconds
- this.playLoop = true;
- }
+ // calculate center, the date to zoom around
+ var gesture = util.fakeGesture(this, event),
+ pointer = getPointer(gesture.center, this.body.dom.center),
+ pointerDate = this._pointerToDate(pointer);
+
+ this.zoom(scale, pointerDate);
+ }
+
+ // Prevent default actions caused by mouse wheel
+ // (else the page and timeline both zoom and scroll)
+ event.preventDefault();
+ };
/**
- * Select the previous index
+ * Start of a touch gesture
+ * @private
*/
- Slider.prototype.prev = function() {
- var index = this.getIndex();
- if (index > 0) {
- index--;
- this.setIndex(index);
- }
+ Range.prototype._onTouch = function (event) {
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
+ this.props.touch.allowDragging = true;
+ this.props.touch.center = null;
};
/**
- * Select the next index
+ * On start of a hold gesture
+ * @private
*/
- Slider.prototype.next = function() {
- var index = this.getIndex();
- if (index < this.values.length - 1) {
- index++;
- this.setIndex(index);
- }
+ Range.prototype._onHold = function () {
+ this.props.touch.allowDragging = false;
};
/**
- * Select the next index
+ * Handle pinch event
+ * @param {Event} event
+ * @private
*/
- Slider.prototype.playNext = function() {
- var start = new Date();
+ Range.prototype._onPinch = function (event) {
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
- var index = this.getIndex();
- if (index < this.values.length - 1) {
- index++;
- this.setIndex(index);
- }
- else if (this.playLoop) {
- // jump to the start
- index = 0;
- this.setIndex(index);
- }
+ this.props.touch.allowDragging = false;
- var end = new Date();
- var diff = (end - start);
+ if (event.gesture.touches.length > 1) {
+ if (!this.props.touch.center) {
+ this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
+ }
- // calculate how much time it to to set the index and to execute the callback
- // function.
- var interval = Math.max(this.playInterval - diff, 0);
- // document.title = diff // TODO: cleanup
+ var scale = 1 / event.gesture.scale,
+ initDate = this._pointerToDate(this.props.touch.center);
- var me = this;
- this.playTimeout = setTimeout(function() {me.playNext();}, interval);
- };
+ // calculate new start and end
+ var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
+ var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
- /**
- * Toggle start or stop playing
- */
- Slider.prototype.togglePlay = function() {
- if (this.playTimeout === undefined) {
- this.play();
- } else {
- this.stop();
+ // apply new range
+ this.setRange(newStart, newEnd);
}
};
/**
- * Start playing
+ * Helper function to calculate the center date for zooming
+ * @param {{x: Number, y: Number}} pointer
+ * @return {number} date
+ * @private
*/
- Slider.prototype.play = function() {
- // Test whether already playing
- if (this.playTimeout) return;
+ Range.prototype._pointerToDate = function (pointer) {
+ var conversion;
+ var direction = this.options.direction;
- this.playNext();
+ validateDirection(direction);
- if (this.frame) {
- this.frame.play.value = 'Stop';
+ if (direction == 'horizontal') {
+ var width = this.body.domProps.center.width;
+ conversion = this.conversion(width);
+ return pointer.x / conversion.scale + conversion.offset;
}
- };
-
- /**
- * Stop playing
- */
- Slider.prototype.stop = function() {
- clearInterval(this.playTimeout);
- this.playTimeout = undefined;
-
- if (this.frame) {
- this.frame.play.value = 'Play';
+ else {
+ var height = this.body.domProps.center.height;
+ conversion = this.conversion(height);
+ return pointer.y / conversion.scale + conversion.offset;
}
};
/**
- * Set a callback function which will be triggered when the value of the
- * slider bar has changed.
+ * Get the pointer location relative to the location of the dom element
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @param {Element} element HTML DOM element
+ * @return {{x: Number, y: Number}} pointer
+ * @private
*/
- Slider.prototype.setOnChangeCallback = function(callback) {
- this.onChangeCallback = callback;
- };
+ function getPointer (touch, element) {
+ return {
+ x: touch.pageX - util.getAbsoluteLeft(element),
+ y: touch.pageY - util.getAbsoluteTop(element)
+ };
+ }
/**
- * Set the interval for playing the list
- * @param {Number} interval The interval in milliseconds
+ * Zoom the range the given scale in or out. Start and end date will
+ * be adjusted, and the timeline will be redrawn. You can optionally give a
+ * date around which to zoom.
+ * For example, try scale = 0.9 or 1.1
+ * @param {Number} scale Scaling factor. Values above 1 will zoom out,
+ * values below 1 will zoom in.
+ * @param {Number} [center] Value representing a date around which will
+ * be zoomed.
*/
- Slider.prototype.setPlayInterval = function(interval) {
- this.playInterval = interval;
+ Range.prototype.zoom = function(scale, center) {
+ // if centerDate is not provided, take it half between start Date and end Date
+ if (center == null) {
+ center = (this.start + this.end) / 2;
+ }
+
+ // calculate new start and end
+ var newStart = center + (this.start - center) * scale;
+ var newEnd = center + (this.end - center) * scale;
+
+ this.setRange(newStart, newEnd);
};
/**
- * Retrieve the current play interval
- * @return {Number} interval The interval in milliseconds
+ * Move the range with a given delta to the left or right. Start and end
+ * value will be adjusted. For example, try delta = 0.1 or -0.1
+ * @param {Number} delta Moving amount. Positive value will move right,
+ * negative value will move left
*/
- Slider.prototype.getPlayInterval = function(interval) {
- return this.playInterval;
+ Range.prototype.move = function(delta) {
+ // zoom start Date and end Date relative to the centerDate
+ var diff = (this.end - this.start);
+
+ // apply new values
+ var newStart = this.start + diff * delta;
+ var newEnd = this.end + diff * delta;
+
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
};
/**
- * Set looping on or off
- * @pararm {boolean} doLoop If true, the slider will jump to the start when
- * the end is passed, and will jump to the end
- * when the start is passed.
+ * Move the range to a new center point
+ * @param {Number} moveTo New center point of the range
*/
- Slider.prototype.setPlayLoop = function(doLoop) {
- this.playLoop = doLoop;
+ Range.prototype.moveTo = function(moveTo) {
+ var center = (this.start + this.end) / 2;
+
+ var diff = center - moveTo;
+
+ // calculate new start and end
+ var newStart = this.start - diff;
+ var newEnd = this.end - diff;
+
+ this.setRange(newStart, newEnd);
};
+ module.exports = Range;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // Utility functions for ordering and stacking of items
+ var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
/**
- * Execute the onchange callback function
+ * Order items by their start data
+ * @param {Item[]} items
*/
- Slider.prototype.onChange = function() {
- if (this.onChangeCallback !== undefined) {
- this.onChangeCallback();
- }
+ exports.orderByStart = function(items) {
+ items.sort(function (a, b) {
+ return a.data.start - b.data.start;
+ });
};
/**
- * redraw the slider on the correct place
+ * Order items by their end date. If they have no end date, their start date
+ * is used.
+ * @param {Item[]} items
*/
- Slider.prototype.redraw = function() {
- if (this.frame) {
- // resize the bar
- this.frame.bar.style.top = (this.frame.clientHeight/2 -
- this.frame.bar.offsetHeight/2) + 'px';
- this.frame.bar.style.width = (this.frame.clientWidth -
- this.frame.prev.clientWidth -
- this.frame.play.clientWidth -
- this.frame.next.clientWidth - 30) + 'px';
+ exports.orderByEnd = function(items) {
+ items.sort(function (a, b) {
+ var aTime = ('end' in a.data) ? a.data.end : a.data.start,
+ bTime = ('end' in b.data) ? b.data.end : b.data.start;
- // position the slider button
- var left = this.indexToLeft(this.index);
- this.frame.slide.style.left = (left) + 'px';
- }
+ return aTime - bTime;
+ });
};
-
/**
- * Set the list with values for the slider
- * @param {Array} values A javascript array with values (any type)
+ * Adjust vertical positions of the items such that they don't overlap each
+ * other.
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: number, axis: number}} margin
+ * Margins between items and between items and the axis.
+ * @param {boolean} [force=false]
+ * If true, all items will be repositioned. If false (default), only
+ * items having a top===null will be re-stacked
*/
- Slider.prototype.setValues = function(values) {
- this.values = values;
+ exports.stack = function(items, margin, force) {
+ var i, iMax;
- if (this.values.length > 0)
- this.setIndex(0);
- else
- this.index = undefined;
+ if (force) {
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = null;
+ }
+ }
+
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ var item = items[i];
+ if (item.top === null) {
+ // initialize top position
+ item.top = margin.axis;
+
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ var collidingItem = null;
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ var other = items[j];
+ if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) {
+ collidingItem = other;
+ break;
+ }
+ }
+
+ if (collidingItem != null) {
+ // There is a collision. Reposition the items above the colliding element
+ item.top = collidingItem.top + collidingItem.height + margin.item;
+ }
+ } while (collidingItem);
+ }
+ }
};
/**
- * Select a value by its index
- * @param {Number} index
+ * Adjust vertical positions of the items without stacking them
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: number, axis: number}} margin
+ * Margins between items and between items and the axis.
*/
- Slider.prototype.setIndex = function(index) {
- if (index < this.values.length) {
- this.index = index;
+ exports.nostack = function(items, margin) {
+ var i, iMax;
- this.redraw();
- this.onChange();
- }
- else {
- throw 'Error: index out of range';
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = margin.axis;
}
};
/**
- * retrieve the index of the currently selected vaue
- * @return {Number} index
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Item} a The first item
+ * @param {Item} b The second item
+ * @param {Number} margin A minimum required margin.
+ * If margin is provided, the two items will be
+ * marked colliding when they overlap or
+ * when the margin between the two is smaller than
+ * the requested margin.
+ * @return {boolean} true if a and b collide, else false
*/
- Slider.prototype.getIndex = function() {
- return this.index;
+ exports.collision = function(a, b, margin) {
+ return ((a.left - margin + EPSILON) < (b.left + b.width) &&
+ (a.left + a.width + margin - EPSILON) > b.left &&
+ (a.top - margin + EPSILON) < (b.top + b.height) &&
+ (a.top + a.height + margin - EPSILON) > b.top);
};
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var moment = __webpack_require__(39);
+
/**
- * retrieve the currently selected value
- * @return {*} value
+ * @constructor TimeStep
+ * The class TimeStep is an iterator for dates. You provide a start date and an
+ * end date. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
+ *
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- Slider.prototype.get = function() {
- return this.values[this.index];
- };
+ function TimeStep(start, end, minimumStep) {
+ // variables
+ this.current = new Date();
+ this._start = new Date();
+ this._end = new Date();
+ this.autoScale = true;
+ this.scale = TimeStep.SCALE.DAY;
+ this.step = 1;
- Slider.prototype._onMouseDown = function(event) {
- // only react on left mouse button down
- var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
- if (!leftButtonDown) return;
-
- this.startClientX = event.clientX;
- this.startSlideX = parseFloat(this.frame.slide.style.left);
-
- this.frame.style.cursor = 'move';
+ // initialize the range
+ this.setRange(start, end, minimumStep);
+ }
- // add event listeners to handle moving the contents
- // we store the function onmousemove and onmouseup in the graph, so we can
- // remove the eventlisteners lateron in the function mouseUp()
- var me = this;
- this.onmousemove = function (event) {me._onMouseMove(event);};
- this.onmouseup = function (event) {me._onMouseUp(event);};
- G3DaddEventListener(document, 'mousemove', this.onmousemove);
- G3DaddEventListener(document, 'mouseup', this.onmouseup);
- G3DpreventDefault(event);
+ /// enum scale
+ TimeStep.SCALE = {
+ MILLISECOND: 1,
+ SECOND: 2,
+ MINUTE: 3,
+ HOUR: 4,
+ DAY: 5,
+ WEEKDAY: 6,
+ MONTH: 7,
+ YEAR: 8
};
- Slider.prototype.leftToIndex = function (left) {
- var width = parseFloat(this.frame.bar.style.width) -
- this.frame.slide.clientWidth - 10;
- var x = left - 3;
+ /**
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Date} [start] The start date and time.
+ * @param {Date} [end] The end date and time.
+ * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+ TimeStep.prototype.setRange = function(start, end, minimumStep) {
+ if (!(start instanceof Date) || !(end instanceof Date)) {
+ throw "No legal start or end date in method setRange";
+ }
- var index = Math.round(x / width * (this.values.length-1));
- if (index < 0) index = 0;
- if (index > this.values.length-1) index = this.values.length-1;
+ this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
+ this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
- return index;
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep);
+ }
};
- Slider.prototype.indexToLeft = function (index) {
- var width = parseFloat(this.frame.bar.style.width) -
- this.frame.slide.clientWidth - 10;
-
- var x = index / (this.values.length-1) * width;
- var left = x + 3;
-
- return left;
+ /**
+ * Set the range iterator to the start date.
+ */
+ TimeStep.prototype.first = function() {
+ this.current = new Date(this._start.valueOf());
+ this.roundToMinor();
};
+ /**
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
+ */
+ TimeStep.prototype.roundToMinor = function() {
+ // round to floor
+ // IMPORTANT: we have no breaks in this switch! (this is no bug)
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.scale) {
+ case TimeStep.SCALE.YEAR:
+ this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
+ this.current.setMonth(0);
+ case TimeStep.SCALE.MONTH: this.current.setDate(1);
+ case TimeStep.SCALE.DAY: // intentional fall through
+ case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
+ case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
+ case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
+ case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
+ //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
+ }
-
- Slider.prototype._onMouseMove = function (event) {
- var diff = event.clientX - this.startClientX;
- var x = this.startSlideX + diff;
-
- var index = this.leftToIndex(x);
-
- this.setIndex(index);
-
- G3DpreventDefault();
+ if (this.step != 1) {
+ // round down to the first minor value that is a multiple of the current step size
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
+ default: break;
+ }
+ }
};
-
- Slider.prototype._onMouseUp = function (event) {
- this.frame.style.cursor = 'auto';
-
- // remove event listeners
- G3DremoveEventListener(document, 'mousemove', this.onmousemove);
- G3DremoveEventListener(document, 'mouseup', this.onmouseup);
-
- G3DpreventDefault();
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ TimeStep.prototype.hasNext = function () {
+ return (this.current.valueOf() <= this._end.valueOf());
};
+ /**
+ * Do the next step
+ */
+ TimeStep.prototype.next = function() {
+ var prev = this.current.valueOf();
+ // Two cases, needed to prevent issues with switching daylight savings
+ // (end of March and end of October)
+ if (this.current.getMonth() < 6) {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:
- /**--------------------------------------------------------------------------**/
-
+ this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
+ case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
+ case TimeStep.SCALE.HOUR:
+ this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
+ // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
+ var h = this.current.getHours();
+ this.current.setHours(h - (h % this.step));
+ break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
+ }
+ else {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
+ }
+ if (this.step != 1) {
+ // round down to the correct major value
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
+ case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
+ case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
+ case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
+ case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
+ case TimeStep.SCALE.YEAR: break; // nothing to do for year
+ default: break;
+ }
+ }
- /**
- * Retrieve the absolute left value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} left The absolute left position of this element
- * in the browser page.
- */
- getAbsoluteLeft = function(elem) {
- var left = 0;
- while( elem !== null ) {
- left += elem.offsetLeft;
- left -= elem.scrollLeft;
- elem = elem.offsetParent;
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current.valueOf() == prev) {
+ this.current = new Date(this._end.valueOf());
}
- return left;
};
+
/**
- * Retrieve the absolute top value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} top The absolute top position of this element
- * in the browser page.
+ * Get the current datetime
+ * @return {Date} current The current date
*/
- getAbsoluteTop = function(elem) {
- var top = 0;
- while( elem !== null ) {
- top += elem.offsetTop;
- top -= elem.scrollTop;
- elem = elem.offsetParent;
- }
- return top;
+ TimeStep.prototype.getCurrent = function() {
+ return this.current;
};
/**
- * Get the horizontal mouse position from a mouse event
- * @param {Event} event
- * @return {Number} mouse x
+ * Set a custom scale. Autoscaling will be disabled.
+ * For example setScale(SCALE.MINUTES, 5) will result
+ * in minor steps of 5 minutes, and major steps of an hour.
+ *
+ * @param {TimeStep.SCALE} newScale
+ * A scale. Choose from SCALE.MILLISECOND,
+ * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
+ * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
+ * SCALE.YEAR.
+ * @param {Number} newStep A step size, by default 1. Choose for
+ * example 1, 2, 5, or 10.
*/
- getMouseX = function(event) {
- if ('clientX' in event) return event.clientX;
- return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
+ TimeStep.prototype.setScale = function(newScale, newStep) {
+ this.scale = newScale;
+
+ if (newStep > 0) {
+ this.step = newStep;
+ }
+
+ this.autoScale = false;
};
/**
- * Get the vertical mouse position from a mouse event
- * @param {Event} event
- * @return {Number} mouse y
+ * Enable or disable autoscaling
+ * @param {boolean} enable If true, autoascaling is set true
*/
- getMouseY = function(event) {
- if ('clientY' in event) return event.clientY;
- return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
+ TimeStep.prototype.setAutoScale = function (enable) {
+ this.autoScale = enable;
};
- module.exports = Graph3d;
-
-
-/***/ },
-/* 6 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Emitter = __webpack_require__(41);
- var Hammer = __webpack_require__(49);
- var util = __webpack_require__(1);
- var DataSet = __webpack_require__(3);
- var DataView = __webpack_require__(4);
- var Range = __webpack_require__(9);
- var TimeAxis = __webpack_require__(21);
- var CurrentTime = __webpack_require__(13);
- var CustomTime = __webpack_require__(14);
- var ItemSet = __webpack_require__(18);
/**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {Object} [options] See Timeline.setOptions for the available options.
- * @constructor
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- function Timeline (container, items, options) {
- if (!(this instanceof Timeline)) {
- throw new SyntaxError('Constructor must be called with the new operator');
+ TimeStep.prototype.setMinimumStep = function(minimumStep) {
+ if (minimumStep == undefined) {
+ return;
}
- var me = this;
- this.defaultOptions = {
- start: null,
- end: null,
-
- autoResize: true,
-
- orientation: 'bottom',
- width: null,
- height: null,
- maxHeight: null,
- minHeight: null
- };
- this.options = util.deepExtend({}, this.defaultOptions);
+ var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
+ var stepMonth = (1000 * 60 * 60 * 24 * 30);
+ var stepDay = (1000 * 60 * 60 * 24);
+ var stepHour = (1000 * 60 * 60);
+ var stepMinute = (1000 * 60);
+ var stepSecond = (1000);
+ var stepMillisecond= (1);
- // Create the DOM, props, and emitter
- this._create(container);
+ // find the smallest step that is larger than the provided minimumStep
+ if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
+ if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
+ if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
+ if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
+ if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
+ if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
+ if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
+ if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
+ if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
+ if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
+ if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
+ if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
+ if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
+ if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
+ if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
+ if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
+ if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
+ if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
+ if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
+ if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
+ if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
+ if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
+ if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
+ if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
+ if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
+ if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
+ if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
+ if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
+ if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
+ };
- // all components listed here will be repainted automatically
- this.components = [];
+ /**
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
+ */
+ TimeStep.prototype.snap = function(date) {
+ var clone = new Date(date.valueOf());
- this.body = {
- dom: this.dom,
- domProps: this.props,
- emitter: {
- on: this.on.bind(this),
- off: this.off.bind(this),
- emit: this.emit.bind(this)
- },
- util: {
- snap: null, // will be specified after TimeAxis is created
- toScreen: me._toScreen.bind(me),
- toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
- toTime: me._toTime.bind(me),
- toGlobalTime : me._toGlobalTime.bind(me)
+ if (this.scale == TimeStep.SCALE.YEAR) {
+ var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
+ clone.setFullYear(Math.round(year / this.step) * this.step);
+ clone.setMonth(0);
+ clone.setDate(0);
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.MONTH) {
+ if (clone.getDate() > 15) {
+ clone.setDate(1);
+ clone.setMonth(clone.getMonth() + 1);
+ // important: first set Date to 1, after that change the month.
+ }
+ else {
+ clone.setDate(1);
}
- };
-
- // range
- this.range = new Range(this.body);
- this.components.push(this.range);
- this.body.range = this.range;
-
- // time axis
- this.timeAxis = new TimeAxis(this.body);
- this.components.push(this.timeAxis);
- this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
-
- // current time bar
- this.currentTime = new CurrentTime(this.body);
- this.components.push(this.currentTime);
-
- // custom time bar
- // Note: time bar will be attached in this.setOptions when selected
- this.customTime = new CustomTime(this.body);
- this.components.push(this.customTime);
-
- // item set
- this.itemSet = new ItemSet(this.body);
- this.components.push(this.itemSet);
-
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
- // apply options
- if (options) {
- this.setOptions(options);
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
}
-
- // create itemset
- if (items) {
- this.setItems(items);
+ else if (this.scale == TimeStep.SCALE.DAY) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
+ default:
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
+ }
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
}
- else {
- this.redraw();
+ else if (this.scale == TimeStep.SCALE.WEEKDAY) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
+ default:
+ clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
+ }
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
}
- }
-
- // turn Timeline into an event emitter
- Emitter(Timeline.prototype);
+ else if (this.scale == TimeStep.SCALE.HOUR) {
+ switch (this.step) {
+ case 4:
+ clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
+ default:
+ clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
+ }
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ } else if (this.scale == TimeStep.SCALE.MINUTE) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
+ clone.setSeconds(0);
+ break;
+ case 5:
+ clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
+ default:
+ clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
+ }
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.SECOND) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
+ clone.setMilliseconds(0);
+ break;
+ case 5:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
+ default:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
+ }
+ }
+ else if (this.scale == TimeStep.SCALE.MILLISECOND) {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
+ }
+
+ return clone;
+ };
/**
- * Create the main DOM for the Timeline: a root panel containing left, right,
- * top, bottom, content, and background panel.
- * @param {Element} container The container element where the Timeline will
- * be attached.
- * @private
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
*/
- Timeline.prototype._create = function (container) {
- this.dom = {};
-
- this.dom.root = document.createElement('div');
- this.dom.background = document.createElement('div');
- this.dom.backgroundVertical = document.createElement('div');
- this.dom.backgroundHorizontal = document.createElement('div');
- this.dom.centerContainer = document.createElement('div');
- this.dom.leftContainer = document.createElement('div');
- this.dom.rightContainer = document.createElement('div');
- this.dom.center = document.createElement('div');
- this.dom.left = document.createElement('div');
- this.dom.right = document.createElement('div');
- this.dom.top = document.createElement('div');
- this.dom.bottom = document.createElement('div');
- this.dom.shadowTop = document.createElement('div');
- this.dom.shadowBottom = document.createElement('div');
- this.dom.shadowTopLeft = document.createElement('div');
- this.dom.shadowBottomLeft = document.createElement('div');
- this.dom.shadowTopRight = document.createElement('div');
- this.dom.shadowBottomRight = document.createElement('div');
-
- this.dom.background.className = 'vispanel background';
- this.dom.backgroundVertical.className = 'vispanel background vertical';
- this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
- this.dom.centerContainer.className = 'vispanel center';
- this.dom.leftContainer.className = 'vispanel left';
- this.dom.rightContainer.className = 'vispanel right';
- this.dom.top.className = 'vispanel top';
- this.dom.bottom.className = 'vispanel bottom';
- this.dom.left.className = 'content';
- this.dom.center.className = 'content';
- this.dom.right.className = 'content';
- this.dom.shadowTop.className = 'shadow top';
- this.dom.shadowBottom.className = 'shadow bottom';
- this.dom.shadowTopLeft.className = 'shadow top';
- this.dom.shadowBottomLeft.className = 'shadow bottom';
- this.dom.shadowTopRight.className = 'shadow top';
- this.dom.shadowBottomRight.className = 'shadow bottom';
-
- this.dom.root.appendChild(this.dom.background);
- this.dom.root.appendChild(this.dom.backgroundVertical);
- this.dom.root.appendChild(this.dom.backgroundHorizontal);
- this.dom.root.appendChild(this.dom.centerContainer);
- this.dom.root.appendChild(this.dom.leftContainer);
- this.dom.root.appendChild(this.dom.rightContainer);
- this.dom.root.appendChild(this.dom.top);
- this.dom.root.appendChild(this.dom.bottom);
-
- this.dom.centerContainer.appendChild(this.dom.center);
- this.dom.leftContainer.appendChild(this.dom.left);
- this.dom.rightContainer.appendChild(this.dom.right);
-
- this.dom.centerContainer.appendChild(this.dom.shadowTop);
- this.dom.centerContainer.appendChild(this.dom.shadowBottom);
- this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
- this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
- this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
- this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
-
- this.on('rangechange', this.redraw.bind(this));
- this.on('change', this.redraw.bind(this));
- this.on('touch', this._onTouch.bind(this));
- this.on('pinch', this._onPinch.bind(this));
- this.on('dragstart', this._onDragStart.bind(this));
- this.on('drag', this._onDrag.bind(this));
-
- // create event listeners for all interesting events, these events will be
- // emitted via emitter
- this.hammer = Hammer(this.dom.root, {
- prevent_default: true
- });
- this.listeners = {};
-
- var me = this;
- var events = [
- 'touch', 'pinch',
- 'tap', 'doubletap', 'hold',
- 'dragstart', 'drag', 'dragend',
- 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
- ];
- events.forEach(function (event) {
- var listener = function () {
- var args = [event].concat(Array.prototype.slice.call(arguments, 0));
- me.emit.apply(me, args);
- };
- me.hammer.on(event, listener);
- me.listeners[event] = listener;
- });
-
- // size properties of each of the panels
- this.props = {
- root: {},
- background: {},
- centerContainer: {},
- leftContainer: {},
- rightContainer: {},
- center: {},
- left: {},
- right: {},
- top: {},
- bottom: {},
- border: {},
- scrollTop: 0,
- scrollTopMin: 0
- };
- this.touch = {}; // store state information needed for touch events
-
- // attach the root panel to the provided container
- if (!container) throw new Error('No container provided');
- container.appendChild(this.dom.root);
+ TimeStep.prototype.isMajor = function() {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:
+ return (this.current.getMilliseconds() == 0);
+ case TimeStep.SCALE.SECOND:
+ return (this.current.getSeconds() == 0);
+ case TimeStep.SCALE.MINUTE:
+ return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
+ // Note: this is no bug. Major label is equal for both minute and hour scale
+ case TimeStep.SCALE.HOUR:
+ return (this.current.getHours() == 0);
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY:
+ return (this.current.getDate() == 1);
+ case TimeStep.SCALE.MONTH:
+ return (this.current.getMonth() == 0);
+ case TimeStep.SCALE.YEAR:
+ return false;
+ default:
+ return false;
+ }
};
+
/**
- * Destroy the Timeline, clean up all DOM elements and event listeners.
+ * Returns formatted text for the minor axislabel, depending on the current
+ * date and the scale. For example when scale is MINUTE, the current time is
+ * formatted as "hh:mm".
+ * @param {Date} [date] custom date. if not provided, current date is taken
*/
- Timeline.prototype.destroy = function () {
- // unbind datasets
- this.clear();
-
- // remove all event listeners
- this.off();
-
- // stop checking for changed size
- this._stopAutoResize();
-
- // remove from DOM
- if (this.dom.root.parentNode) {
- this.dom.root.parentNode.removeChild(this.dom.root);
+ TimeStep.prototype.getLabelMinor = function(date) {
+ if (date == undefined) {
+ date = this.current;
}
- this.dom = null;
- // cleanup hammer touch events
- for (var event in this.listeners) {
- if (this.listeners.hasOwnProperty(event)) {
- delete this.listeners[event];
- }
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
+ case TimeStep.SCALE.SECOND: return moment(date).format('s');
+ case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
+ case TimeStep.SCALE.DAY: return moment(date).format('D');
+ case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
+ case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
+ default: return '';
}
- this.listeners = null;
- this.hammer = null;
-
- // give all components the opportunity to cleanup
- this.components.forEach(function (component) {
- component.destroy();
- });
-
- this.body = null;
};
+
/**
- * Set options. Options will be passed to all components loaded in the Timeline.
- * @param {Object} [options]
- * {String} orientation
- * Vertical orientation for the Timeline,
- * can be 'bottom' (default) or 'top'.
- * {String | Number} width
- * Width for the timeline, a number in pixels or
- * a css string like '1000px' or '75%'. '100%' by default.
- * {String | Number} height
- * Fixed height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'. If undefined,
- * The Timeline will automatically size such that
- * its contents fit.
- * {String | Number} minHeight
- * Minimum height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'.
- * {String | Number} maxHeight
- * Maximum height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'.
- * {Number | Date | String} start
- * Start date for the visible window
- * {Number | Date | String} end
- * End date for the visible window
+ * Returns formatted text for the major axis label, depending on the current
+ * date and the scale. For example when scale is MINUTE, the major scale is
+ * hours, and the hour will be formatted as "hh".
+ * @param {Date} [date] custom date. if not provided, current date is taken
*/
- Timeline.prototype.setOptions = function (options) {
- if (options) {
- // copy the known options
- var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
- util.selectiveExtend(fields, this.options, options);
-
- // enable/disable autoResize
- this._initAutoResize();
+ TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
}
- // propagate options to all components
- this.components.forEach(function (component) {
- component.setOptions(options);
- });
-
- // TODO: remove deprecation error one day (deprecated since version 0.8.0)
- if (options && options.order) {
- throw new Error('Option order is deprecated. There is no replacement for this feature.');
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
+ case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
+ case TimeStep.SCALE.MINUTE:
+ case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
+ case TimeStep.SCALE.WEEKDAY:
+ case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
+ case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
+ case TimeStep.SCALE.YEAR: return '';
+ default: return '';
}
-
- // redraw everything
- this.redraw();
};
- /**
- * Set a custom time bar
- * @param {Date} time
- */
- Timeline.prototype.setCustomTime = function (time) {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
- }
+ module.exports = TimeStep;
- this.customTime.setCustomTime(time);
- };
- /**
- * Retrieve the current custom time.
- * @return {Date} customTime
- */
- Timeline.prototype.getCustomTime = function() {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
- }
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
- return this.customTime.getCustomTime();
- };
+ var Emitter = __webpack_require__(41);
+ var DataSet = __webpack_require__(3);
+ var DataView = __webpack_require__(4);
+ var Point3d = __webpack_require__(33);
+ var Point2d = __webpack_require__(34);
+ var Filter = __webpack_require__(35);
+ var StepNumber = __webpack_require__(36);
/**
- * Set items
- * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
+ * @constructor Graph3d
+ * Graph3d displays data in 3d.
+ *
+ * Graph3d is developed in javascript as a Google Visualization Chart.
+ *
+ * @param {Element} container The DOM element in which the Graph3d will
+ * be created. Normally a div element.
+ * @param {DataSet | DataView | Array} [data]
+ * @param {Object} [options]
*/
- Timeline.prototype.setItems = function(items) {
- var initialLoad = (this.itemsData == null);
-
- // convert to type DataSet when needed
- var newDataSet;
- if (!items) {
- newDataSet = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- newDataSet = items;
- }
- else {
- // turn an array into a dataset
- newDataSet = new DataSet(items, {
- type: {
- start: 'Date',
- end: 'Date'
- }
- });
+ function Graph3d(container, data, options) {
+ if (!(this instanceof Graph3d)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
}
- // set items
- this.itemsData = newDataSet;
- this.itemSet && this.itemSet.setItems(newDataSet);
+ // create variables and set default values
+ this.containerElement = container;
+ this.width = '400px';
+ this.height = '400px';
+ this.margin = 10; // px
+ this.defaultXCenter = '55%';
+ this.defaultYCenter = '50%';
- if (initialLoad && ('start' in this.options || 'end' in this.options)) {
- this.fit();
+ this.xLabel = 'x';
+ this.yLabel = 'y';
+ this.zLabel = 'z';
+ this.filterLabel = 'time';
+ this.legendLabel = 'value';
- var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
- var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
+ this.style = Graph3d.STYLE.DOT;
+ this.showPerspective = true;
+ this.showGrid = true;
+ this.keepAspectRatio = true;
+ this.showShadow = false;
+ this.showGrayBottom = false; // TODO: this does not work correctly
+ this.showTooltip = false;
+ this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
- this.setWindow(start, end);
+ this.animationInterval = 1000; // milliseconds
+ this.animationPreload = false;
+
+ this.camera = new Graph3d.Camera();
+ this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
+
+ this.dataTable = null; // The original data table
+ this.dataPoints = null; // The table with point objects
+
+ // the column indexes
+ this.colX = undefined;
+ this.colY = undefined;
+ this.colZ = undefined;
+ this.colValue = undefined;
+ this.colFilter = undefined;
+
+ this.xMin = 0;
+ this.xStep = undefined; // auto by default
+ this.xMax = 1;
+ this.yMin = 0;
+ this.yStep = undefined; // auto by default
+ this.yMax = 1;
+ this.zMin = 0;
+ this.zStep = undefined; // auto by default
+ this.zMax = 1;
+ this.valueMin = 0;
+ this.valueMax = 1;
+ this.xBarWidth = 1;
+ this.yBarWidth = 1;
+ // TODO: customize axis range
+
+ // constants
+ this.colorAxis = '#4D4D4D';
+ this.colorGrid = '#D3D3D3';
+ this.colorDot = '#7DC1FF';
+ this.colorDotBorder = '#3267D2';
+
+ // create a frame and canvas
+ this.create();
+
+ // apply options (also when undefined)
+ this.setOptions(options);
+
+ // apply data
+ if (data) {
+ this.setData(data);
}
+ }
+
+ // Extend Graph3d with an Emitter mixin
+ Emitter(Graph3d.prototype);
+
+ /**
+ * @class Camera
+ * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
+ * The camera is always looking in the direction of the origin of the arm.
+ * This way, the camera always rotates around one fixed point, the location
+ * of the camera arm.
+ *
+ * Documentation:
+ * http://en.wikipedia.org/wiki/3D_projection
+ */
+ Graph3d.Camera = function () {
+ this.armLocation = new Point3d();
+ this.armRotation = {};
+ this.armRotation.horizontal = 0;
+ this.armRotation.vertical = 0;
+ this.armLength = 1.7;
+
+ this.cameraLocation = new Point3d();
+ this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
+
+ this.calculateCameraOrientation();
};
/**
- * Set groups
- * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ * Set the location (origin) of the arm
+ * @param {Number} x Normalized value of x
+ * @param {Number} y Normalized value of y
+ * @param {Number} z Normalized value of z
*/
- Timeline.prototype.setGroups = function(groups) {
- // convert to type DataSet when needed
- var newDataSet;
- if (!groups) {
- newDataSet = null;
- }
- else if (groups instanceof DataSet || groups instanceof DataView) {
- newDataSet = groups;
- }
- else {
- // turn an array into a dataset
- newDataSet = new DataSet(groups);
- }
+ Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
+ this.armLocation.x = x;
+ this.armLocation.y = y;
+ this.armLocation.z = z;
- this.groupsData = newDataSet;
- this.itemSet.setGroups(newDataSet);
+ this.calculateCameraOrientation();
};
/**
- * Clear the Timeline. By Default, items, groups and options are cleared.
- * Example usage:
- *
- * timeline.clear(); // clear items, groups, and options
- * timeline.clear({options: true}); // clear options only
- *
- * @param {Object} [what] Optionally specify what to clear. By default:
- * {items: true, groups: true, options: true}
+ * Set the rotation of the camera arm
+ * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
*/
- Timeline.prototype.clear = function(what) {
- // clear items
- if (!what || what.items) {
- this.setItems(null);
+ Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
+ if (horizontal !== undefined) {
+ this.armRotation.horizontal = horizontal;
}
- // clear groups
- if (!what || what.groups) {
- this.setGroups(null);
+ if (vertical !== undefined) {
+ this.armRotation.vertical = vertical;
+ if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
+ if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
}
- // clear options of timeline and of each of the components
- if (!what || what.options) {
- this.components.forEach(function (component) {
- component.setOptions(component.defaultOptions);
- });
-
- this.setOptions(this.defaultOptions); // this will also do a redraw
+ if (horizontal !== undefined || vertical !== undefined) {
+ this.calculateCameraOrientation();
}
};
/**
- * Set Timeline window such that it fits all items
+ * Retrieve the current arm rotation
+ * @return {object} An object with parameters horizontal and vertical
*/
- Timeline.prototype.fit = function() {
- // apply the data range as range
- var dataRange = this.getItemRange();
+ Graph3d.Camera.prototype.getArmRotation = function() {
+ var rot = {};
+ rot.horizontal = this.armRotation.horizontal;
+ rot.vertical = this.armRotation.vertical;
- // add 5% space on both sides
- var start = dataRange.min;
- var end = dataRange.max;
- if (start != null && end != null) {
- var interval = (end.valueOf() - start.valueOf());
- if (interval <= 0) {
- // prevent an empty interval
- interval = 24 * 60 * 60 * 1000; // 1 day
- }
- start = new Date(start.valueOf() - interval * 0.05);
- end = new Date(end.valueOf() + interval * 0.05);
- }
+ return rot;
+ };
- // skip range set if there is no start and end date
- if (start === null && end === null) {
+ /**
+ * Set the (normalized) length of the camera arm.
+ * @param {Number} length A length between 0.71 and 5.0
+ */
+ Graph3d.Camera.prototype.setArmLength = function(length) {
+ if (length === undefined)
return;
- }
- this.range.setRange(start, end);
+ this.armLength = length;
+
+ // Radius must be larger than the corner of the graph,
+ // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
+ // graph
+ if (this.armLength < 0.71) this.armLength = 0.71;
+ if (this.armLength > 5.0) this.armLength = 5.0;
+
+ this.calculateCameraOrientation();
};
/**
- * Get the data range of the item set.
- * @returns {{min: Date, max: Date}} range A range with a start and end Date.
- * When no minimum is found, min==null
- * When no maximum is found, max==null
+ * Retrieve the arm length
+ * @return {Number} length
*/
- Timeline.prototype.getItemRange = function() {
- // calculate min from start filed
- var dataset = this.itemsData.getDataSet(),
- min = null,
- max = null;
+ Graph3d.Camera.prototype.getArmLength = function() {
+ return this.armLength;
+ };
- if (dataset) {
- // calculate the minimum value of the field 'start'
- var minItem = dataset.min('start');
- min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
- // Note: we convert first to Date and then to number because else
- // a conversion from ISODate to Number will fail
-
- // calculate maximum value of fields 'start' and 'end'
- var maxStartItem = dataset.max('start');
- if (maxStartItem) {
- max = util.convert(maxStartItem.start, 'Date').valueOf();
- }
- var maxEndItem = dataset.max('end');
- if (maxEndItem) {
- if (max == null) {
- max = util.convert(maxEndItem.end, 'Date').valueOf();
- }
- else {
- max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
- }
- }
- }
-
- return {
- min: (min != null) ? new Date(min) : null,
- max: (max != null) ? new Date(max) : null
- };
+ /**
+ * Retrieve the camera location
+ * @return {Point3d} cameraLocation
+ */
+ Graph3d.Camera.prototype.getCameraLocation = function() {
+ return this.cameraLocation;
};
/**
- * Set selected items by their id. Replaces the current selection
- * Unknown id's are silently ignored.
- * @param {Array} [ids] An array with zero or more id's of the items to be
- * selected. If ids is an empty array, all items will be
- * unselected.
+ * Retrieve the camera rotation
+ * @return {Point3d} cameraRotation
*/
- Timeline.prototype.setSelection = function(ids) {
- this.itemSet && this.itemSet.setSelection(ids);
+ Graph3d.Camera.prototype.getCameraRotation = function() {
+ return this.cameraRotation;
};
/**
- * Get the selected items by their id
- * @return {Array} ids The ids of the selected items
+ * Calculate the location and rotation of the camera based on the
+ * position and orientation of the camera arm
*/
- Timeline.prototype.getSelection = function() {
- return this.itemSet && this.itemSet.getSelection() || [];
+ Graph3d.Camera.prototype.calculateCameraOrientation = function() {
+ // calculate location of the camera
+ this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
+
+ // calculate rotation of the camera
+ this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
+ this.cameraRotation.y = 0;
+ this.cameraRotation.z = -this.armRotation.horizontal;
};
/**
- * Set the visible window. Both parameters are optional, you can change only
- * start or only end. Syntax:
- *
- * TimeLine.setWindow(start, end)
- * TimeLine.setWindow(range)
- *
- * Where start and end can be a Date, number, or string, and range is an
- * object with properties start and end.
- *
- * @param {Date | Number | String | Object} [start] Start date of visible window
- * @param {Date | Number | String} [end] End date of visible window
+ * Calculate the scaling values, dependent on the range in x, y, and z direction
*/
- Timeline.prototype.setWindow = function(start, end) {
- if (arguments.length == 1) {
- var range = arguments[0];
- this.range.setRange(range.start, range.end);
- }
- else {
- this.range.setRange(start, end);
+ Graph3d.prototype._setScale = function() {
+ this.scale = new Point3d(1 / (this.xMax - this.xMin),
+ 1 / (this.yMax - this.yMin),
+ 1 / (this.zMax - this.zMin));
+
+ // keep aspect ration between x and y scale if desired
+ if (this.keepAspectRatio) {
+ if (this.scale.x < this.scale.y) {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.y = this.scale.x;
+ }
+ else {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.x = this.scale.y;
+ }
}
+
+ // scale the vertical axis
+ this.scale.z *= this.verticalRatio;
+ // TODO: can this be automated? verticalRatio?
+
+ // determine scale for (optional) value
+ this.scale.value = 1 / (this.valueMax - this.valueMin);
+
+ // position the camera arm
+ var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
+ var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
+ var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
+ this.camera.setArmLocation(xCenter, yCenter, zCenter);
};
+
/**
- * Get the visible window
- * @return {{start: Date, end: Date}} Visible range
+ * Convert a 3D location to a 2D location on screen
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point2d} point2d A 2D point with parameters x, y
*/
- Timeline.prototype.getWindow = function() {
- var range = this.range.getRange();
- return {
- start: new Date(range.start),
- end: new Date(range.end)
- };
+ Graph3d.prototype._convert3Dto2D = function(point3d) {
+ var translation = this._convertPointToTranslation(point3d);
+ return this._convertTranslationToScreen(translation);
};
/**
- * Force a redraw of the Timeline. Can be useful to manually redraw when
- * option autoResize=false
+ * Convert a 3D location its translation seen from the camera
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
*/
- Timeline.prototype.redraw = function() {
- var resized = false,
- options = this.options,
- props = this.props,
- dom = this.dom;
+ Graph3d.prototype._convertPointToTranslation = function(point3d) {
+ var ax = point3d.x * this.scale.x,
+ ay = point3d.y * this.scale.y,
+ az = point3d.z * this.scale.z,
- if (!dom) return; // when destroyed
+ cx = this.camera.getCameraLocation().x,
+ cy = this.camera.getCameraLocation().y,
+ cz = this.camera.getCameraLocation().z,
- // update class names
- dom.root.className = 'vis timeline root ' + options.orientation;
+ // calculate angles
+ sinTx = Math.sin(this.camera.getCameraRotation().x),
+ cosTx = Math.cos(this.camera.getCameraRotation().x),
+ sinTy = Math.sin(this.camera.getCameraRotation().y),
+ cosTy = Math.cos(this.camera.getCameraRotation().y),
+ sinTz = Math.sin(this.camera.getCameraRotation().z),
+ cosTz = Math.cos(this.camera.getCameraRotation().z),
- // update root width and height options
- dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
- dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
- dom.root.style.width = util.option.asSize(options.width, '');
+ // calculate translation
+ dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
+ dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
+ dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
- // calculate border widths
- props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
- props.border.right = props.border.left;
- props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
- props.border.bottom = props.border.top;
- var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
- var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
+ return new Point3d(dx, dy, dz);
+ };
- // calculate the heights. If any of the side panels is empty, we set the height to
- // minus the border width, such that the border will be invisible
- props.center.height = dom.center.offsetHeight;
- props.left.height = dom.left.offsetHeight;
- props.right.height = dom.right.offsetHeight;
- props.top.height = dom.top.clientHeight || -props.border.top;
- props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+ /**
+ * Convert a translation point to a point on the screen
+ * @param {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
+ * @return {Point2d} point2d A 2D point with parameters x, y
+ */
+ Graph3d.prototype._convertTranslationToScreen = function(translation) {
+ var ex = this.eye.x,
+ ey = this.eye.y,
+ ez = this.eye.z,
+ dx = translation.x,
+ dy = translation.y,
+ dz = translation.z;
- // TODO: compensate borders when any of the panels is empty.
+ // calculate position on screen from translation
+ var bx;
+ var by;
+ if (this.showPerspective) {
+ bx = (dx - ex) * (ez / dz);
+ by = (dy - ey) * (ez / dz);
+ }
+ else {
+ bx = dx * -(ez / this.camera.getArmLength());
+ by = dy * -(ez / this.camera.getArmLength());
+ }
- // apply auto height
- // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
- var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
- var autoHeight = props.top.height + contentHeight + props.bottom.height +
- borderRootHeight + props.border.top + props.border.bottom;
- dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+ // shift and scale the point to the center of the screen
+ // use the width of the graph to scale both horizontally and vertically.
+ return new Point2d(
+ this.xcenter + bx * this.frame.canvas.clientWidth,
+ this.ycenter - by * this.frame.canvas.clientWidth);
+ };
- // calculate heights of the content panels
- props.root.height = dom.root.offsetHeight;
- props.background.height = props.root.height - borderRootHeight;
- var containerHeight = props.root.height - props.top.height - props.bottom.height -
- borderRootHeight;
- props.centerContainer.height = containerHeight;
- props.leftContainer.height = containerHeight;
- props.rightContainer.height = props.leftContainer.height;
+ /**
+ * Set the background styling for the graph
+ * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
+ */
+ Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
+ var fill = 'white';
+ var stroke = 'gray';
+ var strokeWidth = 1;
- // calculate the widths of the panels
- props.root.width = dom.root.offsetWidth;
- props.background.width = props.root.width - borderRootWidth;
- props.left.width = dom.leftContainer.clientWidth || -props.border.left;
- props.leftContainer.width = props.left.width;
- props.right.width = dom.rightContainer.clientWidth || -props.border.right;
- props.rightContainer.width = props.right.width;
- var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
- props.center.width = centerWidth;
- props.centerContainer.width = centerWidth;
- props.top.width = centerWidth;
- props.bottom.width = centerWidth;
+ if (typeof(backgroundColor) === 'string') {
+ fill = backgroundColor;
+ stroke = 'none';
+ strokeWidth = 0;
+ }
+ else if (typeof(backgroundColor) === 'object') {
+ if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
+ if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
+ if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
+ }
+ else if (backgroundColor === undefined) {
+ // use use defaults
+ }
+ else {
+ throw 'Unsupported type of backgroundColor';
+ }
- // resize the panels
- dom.background.style.height = props.background.height + 'px';
- dom.backgroundVertical.style.height = props.background.height + 'px';
- dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
- dom.centerContainer.style.height = props.centerContainer.height + 'px';
- dom.leftContainer.style.height = props.leftContainer.height + 'px';
- dom.rightContainer.style.height = props.rightContainer.height + 'px';
-
- dom.background.style.width = props.background.width + 'px';
- dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
- dom.backgroundHorizontal.style.width = props.background.width + 'px';
- dom.centerContainer.style.width = props.center.width + 'px';
- dom.top.style.width = props.top.width + 'px';
- dom.bottom.style.width = props.bottom.width + 'px';
-
- // reposition the panels
- dom.background.style.left = '0';
- dom.background.style.top = '0';
- dom.backgroundVertical.style.left = props.left.width + 'px';
- dom.backgroundVertical.style.top = '0';
- dom.backgroundHorizontal.style.left = '0';
- dom.backgroundHorizontal.style.top = props.top.height + 'px';
- dom.centerContainer.style.left = props.left.width + 'px';
- dom.centerContainer.style.top = props.top.height + 'px';
- dom.leftContainer.style.left = '0';
- dom.leftContainer.style.top = props.top.height + 'px';
- dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
- dom.rightContainer.style.top = props.top.height + 'px';
- dom.top.style.left = props.left.width + 'px';
- dom.top.style.top = '0';
- dom.bottom.style.left = props.left.width + 'px';
- dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
-
- // update the scrollTop, feasible range for the offset can be changed
- // when the height of the Timeline or of the contents of the center changed
- this._updateScrollTop();
-
- // reposition the scrollable contents
- var offset = this.props.scrollTop;
- if (options.orientation == 'bottom') {
- offset += Math.max(this.props.centerContainer.height - this.props.center.height -
- this.props.border.top - this.props.border.bottom, 0);
- }
- dom.center.style.left = '0';
- dom.center.style.top = offset + 'px';
- dom.left.style.left = '0';
- dom.left.style.top = offset + 'px';
- dom.right.style.left = '0';
- dom.right.style.top = offset + 'px';
-
- // show shadows when vertical scrolling is available
- var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
- var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
- dom.shadowTop.style.visibility = visibilityTop;
- dom.shadowBottom.style.visibility = visibilityBottom;
- dom.shadowTopLeft.style.visibility = visibilityTop;
- dom.shadowBottomLeft.style.visibility = visibilityBottom;
- dom.shadowTopRight.style.visibility = visibilityTop;
- dom.shadowBottomRight.style.visibility = visibilityBottom;
-
- // redraw all components
- this.components.forEach(function (component) {
- resized = component.redraw() || resized;
- });
- if (resized) {
- // keep repainting until all sizes are settled
- this.redraw();
- }
+ this.frame.style.backgroundColor = fill;
+ this.frame.style.borderColor = stroke;
+ this.frame.style.borderWidth = strokeWidth + 'px';
+ this.frame.style.borderStyle = 'solid';
};
- // TODO: deprecated since version 1.1.0, remove some day
- Timeline.prototype.repaint = function () {
- throw new Error('Function repaint is deprecated. Use redraw instead.');
- };
- /**
- * Convert a position on screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- * @private
- */
- // TODO: move this function to Range
- Timeline.prototype._toTime = function(x) {
- var conversion = this.range.conversion(this.props.center.width);
- return new Date(x / conversion.scale + conversion.offset);
+ /// enumerate the available styles
+ Graph3d.STYLE = {
+ BAR: 0,
+ BARCOLOR: 1,
+ BARSIZE: 2,
+ DOT : 3,
+ DOTLINE : 4,
+ DOTCOLOR: 5,
+ DOTSIZE: 6,
+ GRID : 7,
+ LINE: 8,
+ SURFACE : 9
};
-
/**
- * Convert a position on the global screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- * @private
+ * Retrieve the style index from given styleName
+ * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
+ * @return {Number} styleNumber Enumeration value representing the style, or -1
+ * when not found
*/
- // TODO: move this function to Range
- Timeline.prototype._toGlobalTime = function(x) {
- var conversion = this.range.conversion(this.props.root.width);
- return new Date(x / conversion.scale + conversion.offset);
- };
+ Graph3d.prototype._getStyleNumber = function(styleName) {
+ switch (styleName) {
+ case 'dot': return Graph3d.STYLE.DOT;
+ case 'dot-line': return Graph3d.STYLE.DOTLINE;
+ case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
+ case 'dot-size': return Graph3d.STYLE.DOTSIZE;
+ case 'line': return Graph3d.STYLE.LINE;
+ case 'grid': return Graph3d.STYLE.GRID;
+ case 'surface': return Graph3d.STYLE.SURFACE;
+ case 'bar': return Graph3d.STYLE.BAR;
+ case 'bar-color': return Graph3d.STYLE.BARCOLOR;
+ case 'bar-size': return Graph3d.STYLE.BARSIZE;
+ }
- /**
- * Convert a datetime (Date object) into a position on the screen
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
- * @private
- */
- // TODO: move this function to Range
- Timeline.prototype._toScreen = function(time) {
- var conversion = this.range.conversion(this.props.center.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
+ return -1;
};
-
/**
- * Convert a datetime (Date object) into a position on the root
- * This is used to get the pixel density estimate for the screen, not the center panel
- * @param {Date} time A date
- * @return {int} x The position on root in pixels which corresponds
- * with the given date.
- * @private
+ * Determine the indexes of the data columns, based on the given style and data
+ * @param {DataSet} data
+ * @param {Number} style
*/
- // TODO: move this function to Range
- Timeline.prototype._toGlobalScreen = function(time) {
- var conversion = this.range.conversion(this.props.root.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
- };
+ Graph3d.prototype._determineColumnIndexes = function(data, style) {
+ if (this.style === Graph3d.STYLE.DOT ||
+ this.style === Graph3d.STYLE.DOTLINE ||
+ this.style === Graph3d.STYLE.LINE ||
+ this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE ||
+ this.style === Graph3d.STYLE.BAR) {
+ // 3 columns expected, and optionally a 4th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = undefined;
+ if (data.getNumberOfColumns() > 3) {
+ this.colFilter = 3;
+ }
+ }
+ else if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ // 4 columns expected, and optionally a 5th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = 3;
- /**
- * Initialize watching when option autoResize is true
- * @private
- */
- Timeline.prototype._initAutoResize = function () {
- if (this.options.autoResize == true) {
- this._startAutoResize();
+ if (data.getNumberOfColumns() > 4) {
+ this.colFilter = 4;
+ }
}
else {
- this._stopAutoResize();
+ throw 'Unknown style "' + this.style + '"';
}
};
- /**
- * Watch for changes in the size of the container. On resize, the Panel will
- * automatically redraw itself.
- * @private
- */
- Timeline.prototype._startAutoResize = function () {
- var me = this;
+ Graph3d.prototype.getNumberOfRows = function(data) {
+ return data.length;
+ }
- this._stopAutoResize();
- this._onResize = function() {
- if (me.options.autoResize != true) {
- // stop watching when the option autoResize is changed to false
- me._stopAutoResize();
- return;
+ Graph3d.prototype.getNumberOfColumns = function(data) {
+ var counter = 0;
+ for (var column in data[0]) {
+ if (data[0].hasOwnProperty(column)) {
+ counter++;
}
+ }
+ return counter;
+ }
- if (me.dom.root) {
- // check whether the frame is resized
- if ((me.dom.root.clientWidth != me.props.lastWidth) ||
- (me.dom.root.clientHeight != me.props.lastHeight)) {
- me.props.lastWidth = me.dom.root.clientWidth;
- me.props.lastHeight = me.dom.root.clientHeight;
- me.emit('change');
- }
+ Graph3d.prototype.getDistinctValues = function(data, column) {
+ var distinctValues = [];
+ for (var i = 0; i < data.length; i++) {
+ if (distinctValues.indexOf(data[i][column]) == -1) {
+ distinctValues.push(data[i][column]);
}
- };
+ }
+ return distinctValues;
+ }
- // add event listener to window resize
- util.addEventListener(window, 'resize', this._onResize);
- this.watchTimer = setInterval(this._onResize, 1000);
+ Graph3d.prototype.getColumnRange = function(data,column) {
+ var minMax = {min:data[0][column],max:data[0][column]};
+ for (var i = 0; i < data.length; i++) {
+ if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
+ if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
+ }
+ return minMax;
};
/**
- * Stop watching for a resize of the frame.
- * @private
+ * Initialize the data from the data table. Calculate minimum and maximum values
+ * and column index values
+ * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
+ * @param {Number} style Style Number
*/
- Timeline.prototype._stopAutoResize = function () {
- if (this.watchTimer) {
- clearInterval(this.watchTimer);
- this.watchTimer = undefined;
+ Graph3d.prototype._dataInitialize = function (rawData, style) {
+ var me = this;
+
+ // unsubscribe from the dataTable
+ if (this.dataSet) {
+ this.dataSet.off('*', this._onChange);
}
- // remove event listener on window.resize
- util.removeEventListener(window, 'resize', this._onResize);
- this._onResize = null;
- };
+ if (rawData === undefined)
+ return;
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Timeline.prototype._onTouch = function (event) {
- this.touch.allowDragging = true;
- };
+ if (Array.isArray(rawData)) {
+ rawData = new DataSet(rawData);
+ }
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Timeline.prototype._onPinch = function (event) {
- this.touch.allowDragging = false;
- };
+ var data;
+ if (rawData instanceof DataSet || rawData instanceof DataView) {
+ data = rawData.get();
+ }
+ else {
+ throw new Error('Array, DataSet, or DataView expected');
+ }
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Timeline.prototype._onDragStart = function (event) {
- this.touch.initialScrollTop = this.props.scrollTop;
- };
+ if (data.length == 0)
+ return;
- /**
- * Move the timeline vertically
- * @param {Event} event
- * @private
- */
- Timeline.prototype._onDrag = function (event) {
- // 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
- if (!this.touch.allowDragging) return;
+ this.dataSet = rawData;
+ this.dataTable = data;
- var delta = event.gesture.deltaY;
+ // subscribe to changes in the dataset
+ this._onChange = function () {
+ me.setData(me.dataSet);
+ };
+ this.dataSet.on('*', this._onChange);
- var oldScrollTop = this._getScrollTop();
- var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+ // _determineColumnIndexes
+ // getNumberOfRows (points)
+ // getNumberOfColumns (x,y,z,v,t,t1,t2...)
+ // getDistinctValues (unique values?)
+ // getColumnRange
- if (newScrollTop != oldScrollTop) {
- this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
+ // determine the location of x,y,z,value,filter columns
+ this.colX = 'x';
+ this.colY = 'y';
+ this.colZ = 'z';
+ this.colValue = 'style';
+ this.colFilter = 'filter';
+
+
+
+ // check if a filter column is provided
+ if (data[0].hasOwnProperty('filter')) {
+ if (this.dataFilter === undefined) {
+ this.dataFilter = new Filter(rawData, this.colFilter, this);
+ this.dataFilter.setOnLoadCallback(function() {me.redraw();});
+ }
}
- };
- /**
- * Apply a scrollTop
- * @param {Number} scrollTop
- * @returns {Number} scrollTop Returns the applied scrollTop
- * @private
- */
- Timeline.prototype._setScrollTop = function (scrollTop) {
- this.props.scrollTop = scrollTop;
- this._updateScrollTop();
- return this.props.scrollTop;
- };
- /**
- * Update the current scrollTop when the height of the containers has been changed
- * @returns {Number} scrollTop Returns the applied scrollTop
- * @private
- */
- Timeline.prototype._updateScrollTop = function () {
- // recalculate the scrollTopMin
- var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
- if (scrollTopMin != this.props.scrollTopMin) {
- // in case of bottom orientation, change the scrollTop such that the contents
- // do not move relative to the time axis at the bottom
- if (this.options.orientation == 'bottom') {
- this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ var withBars = this.style == Graph3d.STYLE.BAR ||
+ this.style == Graph3d.STYLE.BARCOLOR ||
+ this.style == Graph3d.STYLE.BARSIZE;
+
+ // determine barWidth from data
+ if (withBars) {
+ if (this.defaultXBarWidth !== undefined) {
+ this.xBarWidth = this.defaultXBarWidth;
+ }
+ else {
+ var dataX = this.getDistinctValues(data,this.colX);
+ this.xBarWidth = (dataX[1] - dataX[0]) || 1;
+ }
+
+ if (this.defaultYBarWidth !== undefined) {
+ this.yBarWidth = this.defaultYBarWidth;
+ }
+ else {
+ var dataY = this.getDistinctValues(data,this.colY);
+ this.yBarWidth = (dataY[1] - dataY[0]) || 1;
}
- this.props.scrollTopMin = scrollTopMin;
}
- // limit the scrollTop to the feasible scroll range
- if (this.props.scrollTop > 0) this.props.scrollTop = 0;
- if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+ // calculate minimums and maximums
+ var xRange = this.getColumnRange(data,this.colX);
+ if (withBars) {
+ xRange.min -= this.xBarWidth / 2;
+ xRange.max += this.xBarWidth / 2;
+ }
+ this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
+ this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
+ if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
+ this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
- return this.props.scrollTop;
- };
+ var yRange = this.getColumnRange(data,this.colY);
+ if (withBars) {
+ yRange.min -= this.yBarWidth / 2;
+ yRange.max += this.yBarWidth / 2;
+ }
+ this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
+ this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
+ if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
+ this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
- /**
- * Get the current scrollTop
- * @returns {number} scrollTop
- * @private
- */
- Timeline.prototype._getScrollTop = function () {
- return this.props.scrollTop;
- };
+ var zRange = this.getColumnRange(data,this.colZ);
+ this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
+ this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
+ if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
+ this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
- module.exports = Timeline;
+ if (this.colValue !== undefined) {
+ var valueRange = this.getColumnRange(data,this.colValue);
+ this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
+ this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
+ if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
+ }
+ // set the scale dependent on the ranges.
+ this._setScale();
+ };
-/***/ },
-/* 7 */
-/***/ function(module, exports, __webpack_require__) {
- var Emitter = __webpack_require__(41);
- var Hammer = __webpack_require__(49);
- var util = __webpack_require__(1);
- var DataSet = __webpack_require__(3);
- var DataView = __webpack_require__(4);
- var Range = __webpack_require__(9);
- var TimeAxis = __webpack_require__(21);
- var CurrentTime = __webpack_require__(13);
- var CustomTime = __webpack_require__(14);
- var LineGraph = __webpack_require__(20);
/**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {Object} [options] See Graph2d.setOptions for the available options.
- * @constructor
+ * Filter the data based on the current filter
+ * @param {Array} data
+ * @return {Array} dataPoints Array with point objects which can be drawn on screen
*/
- function Graph2d (container, items, options, groups) {
- var me = this;
- this.defaultOptions = {
- start: null,
- end: null,
-
- autoResize: true,
+ Graph3d.prototype._getDataPoints = function (data) {
+ // TODO: store the created matrix dataPoints in the filters instead of reloading each time
+ var x, y, i, z, obj, point;
- orientation: 'bottom',
- width: null,
- height: null,
- maxHeight: null,
- minHeight: null
- };
- this.options = util.deepExtend({}, this.defaultOptions);
+ var dataPoints = [];
- // Create the DOM, props, and emitter
- this._create(container);
+ if (this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE) {
+ // copy all values from the google data table to a matrix
+ // the provided values are supposed to form a grid of (x,y) positions
- // all components listed here will be repainted automatically
- this.components = [];
+ // create two lists with all present x and y values
+ var dataX = [];
+ var dataY = [];
+ for (i = 0; i < this.getNumberOfRows(data); i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
- this.body = {
- dom: this.dom,
- domProps: this.props,
- emitter: {
- on: this.on.bind(this),
- off: this.off.bind(this),
- emit: this.emit.bind(this)
- },
- util: {
- snap: null, // will be specified after TimeAxis is created
- toScreen: me._toScreen.bind(me),
- toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
- toTime: me._toTime.bind(me),
- toGlobalTime : me._toGlobalTime.bind(me)
+ if (dataX.indexOf(x) === -1) {
+ dataX.push(x);
+ }
+ if (dataY.indexOf(y) === -1) {
+ dataY.push(y);
+ }
}
- };
- // range
- this.range = new Range(this.body);
- this.components.push(this.range);
- this.body.range = this.range;
+ function sortNumber(a, b) {
+ return a - b;
+ }
+ dataX.sort(sortNumber);
+ dataY.sort(sortNumber);
- // time axis
- this.timeAxis = new TimeAxis(this.body);
- this.components.push(this.timeAxis);
- this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
+ // create a grid, a 2d matrix, with all values.
+ var dataMatrix = []; // temporary data matrix
+ for (i = 0; i < data.length; i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
+ z = data[i][this.colZ] || 0;
- // current time bar
- this.currentTime = new CurrentTime(this.body);
- this.components.push(this.currentTime);
+ var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
+ var yIndex = dataY.indexOf(y);
- // custom time bar
- // Note: time bar will be attached in this.setOptions when selected
- this.customTime = new CustomTime(this.body);
- this.components.push(this.customTime);
+ if (dataMatrix[xIndex] === undefined) {
+ dataMatrix[xIndex] = [];
+ }
- // item set
- this.linegraph = new LineGraph(this.body);
- this.components.push(this.linegraph);
+ var point3d = new Point3d();
+ point3d.x = x;
+ point3d.y = y;
+ point3d.z = z;
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
+ obj = {};
+ obj.point = point3d;
+ obj.trans = undefined;
+ obj.screen = undefined;
+ obj.bottom = new Point3d(x, y, this.zMin);
- // apply options
- if (options) {
- this.setOptions(options);
- }
+ dataMatrix[xIndex][yIndex] = obj;
- // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
- if (groups) {
- this.setGroups(groups);
- }
+ dataPoints.push(obj);
+ }
- // create itemset
- if (items) {
- this.setItems(items);
- }
- else {
- this.redraw();
+ // fill in the pointers to the neighbors.
+ for (x = 0; x < dataMatrix.length; x++) {
+ for (y = 0; y < dataMatrix[x].length; y++) {
+ if (dataMatrix[x][y]) {
+ dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
+ dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
+ dataMatrix[x][y].pointCross =
+ (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
+ dataMatrix[x+1][y+1] :
+ undefined;
+ }
+ }
+ }
}
- }
-
- // turn Graph2d into an event emitter
- Emitter(Graph2d.prototype);
+ else { // 'dot', 'dot-line', etc.
+ // copy all values from the google data table to a list with Point3d objects
+ for (i = 0; i < data.length; i++) {
+ point = new Point3d();
+ point.x = data[i][this.colX] || 0;
+ point.y = data[i][this.colY] || 0;
+ point.z = data[i][this.colZ] || 0;
- /**
- * Create the main DOM for the Graph2d: a root panel containing left, right,
- * top, bottom, content, and background panel.
- * @param {Element} container The container element where the Graph2d will
- * be attached.
- * @private
- */
- Graph2d.prototype._create = function (container) {
- this.dom = {};
+ if (this.colValue !== undefined) {
+ point.value = data[i][this.colValue] || 0;
+ }
- this.dom.root = document.createElement('div');
- this.dom.background = document.createElement('div');
- this.dom.backgroundVertical = document.createElement('div');
- this.dom.backgroundHorizontalContainer = document.createElement('div');
- this.dom.centerContainer = document.createElement('div');
- this.dom.leftContainer = document.createElement('div');
- this.dom.rightContainer = document.createElement('div');
- this.dom.backgroundHorizontal = document.createElement('div');
- this.dom.center = document.createElement('div');
- this.dom.left = document.createElement('div');
- this.dom.right = document.createElement('div');
- this.dom.top = document.createElement('div');
- this.dom.bottom = document.createElement('div');
- this.dom.shadowTop = document.createElement('div');
- this.dom.shadowBottom = document.createElement('div');
- this.dom.shadowTopLeft = document.createElement('div');
- this.dom.shadowBottomLeft = document.createElement('div');
- this.dom.shadowTopRight = document.createElement('div');
- this.dom.shadowBottomRight = document.createElement('div');
+ obj = {};
+ obj.point = point;
+ obj.bottom = new Point3d(point.x, point.y, this.zMin);
+ obj.trans = undefined;
+ obj.screen = undefined;
- this.dom.background.className = 'vispanel background';
- this.dom.backgroundVertical.className = 'vispanel background vertical';
- this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
- this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
- this.dom.centerContainer.className = 'vispanel center';
- this.dom.leftContainer.className = 'vispanel left';
- this.dom.rightContainer.className = 'vispanel right';
- this.dom.top.className = 'vispanel top';
- this.dom.bottom.className = 'vispanel bottom';
- this.dom.left.className = 'content';
- this.dom.center.className = 'content';
- this.dom.right.className = 'content';
- this.dom.shadowTop.className = 'shadow top';
- this.dom.shadowBottom.className = 'shadow bottom';
- this.dom.shadowTopLeft.className = 'shadow top';
- this.dom.shadowBottomLeft.className = 'shadow bottom';
- this.dom.shadowTopRight.className = 'shadow top';
- this.dom.shadowBottomRight.className = 'shadow bottom';
+ dataPoints.push(obj);
+ }
+ }
- this.dom.root.appendChild(this.dom.background);
- this.dom.root.appendChild(this.dom.backgroundVertical);
- this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
- this.dom.root.appendChild(this.dom.centerContainer);
- this.dom.root.appendChild(this.dom.leftContainer);
- this.dom.root.appendChild(this.dom.rightContainer);
- this.dom.root.appendChild(this.dom.top);
- this.dom.root.appendChild(this.dom.bottom);
+ return dataPoints;
+ };
- this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
- this.dom.centerContainer.appendChild(this.dom.center);
- this.dom.leftContainer.appendChild(this.dom.left);
- this.dom.rightContainer.appendChild(this.dom.right);
+ /**
+ * Create the main frame for the Graph3d.
+ * This function is executed once when a Graph3d object is created. The frame
+ * contains a canvas, and this canvas contains all objects like the axis and
+ * nodes.
+ */
+ Graph3d.prototype.create = function () {
+ // remove all elements from the container element.
+ while (this.containerElement.hasChildNodes()) {
+ this.containerElement.removeChild(this.containerElement.firstChild);
+ }
- this.dom.centerContainer.appendChild(this.dom.shadowTop);
- this.dom.centerContainer.appendChild(this.dom.shadowBottom);
- this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
- this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
- this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
- this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+ this.frame = document.createElement('div');
+ this.frame.style.position = 'relative';
+ this.frame.style.overflow = 'hidden';
- this.on('rangechange', this.redraw.bind(this));
- this.on('change', this.redraw.bind(this));
- this.on('touch', this._onTouch.bind(this));
- this.on('pinch', this._onPinch.bind(this));
- this.on('dragstart', this._onDragStart.bind(this));
- this.on('drag', this._onDrag.bind(this));
+ // create the graph canvas (HTML canvas element)
+ this.frame.canvas = document.createElement( 'canvas' );
+ this.frame.canvas.style.position = 'relative';
+ this.frame.appendChild(this.frame.canvas);
+ //if (!this.frame.canvas.getContext) {
+ {
+ var noCanvas = document.createElement( 'DIV' );
+ noCanvas.style.color = 'red';
+ noCanvas.style.fontWeight = 'bold' ;
+ noCanvas.style.padding = '10px';
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+ this.frame.canvas.appendChild(noCanvas);
+ }
- // create event listeners for all interesting events, these events will be
- // emitted via emitter
- this.hammer = Hammer(this.dom.root, {
- prevent_default: true
- });
- this.listeners = {};
+ this.frame.filter = document.createElement( 'div' );
+ this.frame.filter.style.position = 'absolute';
+ this.frame.filter.style.bottom = '0px';
+ this.frame.filter.style.left = '0px';
+ this.frame.filter.style.width = '100%';
+ this.frame.appendChild(this.frame.filter);
+ // add event listeners to handle moving and zooming the contents
var me = this;
- var events = [
- 'touch', 'pinch',
- 'tap', 'doubletap', 'hold',
- 'dragstart', 'drag', 'dragend',
- 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
- ];
- events.forEach(function (event) {
- var listener = function () {
- var args = [event].concat(Array.prototype.slice.call(arguments, 0));
- me.emit.apply(me, args);
- };
- me.hammer.on(event, listener);
- me.listeners[event] = listener;
- });
+ var onmousedown = function (event) {me._onMouseDown(event);};
+ var ontouchstart = function (event) {me._onTouchStart(event);};
+ var onmousewheel = function (event) {me._onWheel(event);};
+ var ontooltip = function (event) {me._onTooltip(event);};
+ // TODO: these events are never cleaned up... can give a 'memory leakage'
- // size properties of each of the panels
- this.props = {
- root: {},
- background: {},
- centerContainer: {},
- leftContainer: {},
- rightContainer: {},
- center: {},
- left: {},
- right: {},
- top: {},
- bottom: {},
- border: {},
- scrollTop: 0,
- scrollTopMin: 0
- };
- this.touch = {}; // store state information needed for touch events
+ G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
+ G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
+ G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
+ G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
+ G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
- // attach the root panel to the provided container
- if (!container) throw new Error('No container provided');
- container.appendChild(this.dom.root);
+ // add the new graph to the container element
+ this.containerElement.appendChild(this.frame);
};
+
/**
- * Destroy the Graph2d, clean up all DOM elements and event listeners.
+ * Set a new size for the graph
+ * @param {string} width Width in pixels or percentage (for example '800px'
+ * or '50%')
+ * @param {string} height Height in pixels or percentage (for example '400px'
+ * or '30%')
*/
- Graph2d.prototype.destroy = function () {
- // unbind datasets
- this.clear();
-
- // remove all event listeners
- this.off();
-
- // stop checking for changed size
- this._stopAutoResize();
-
- // remove from DOM
- if (this.dom.root.parentNode) {
- this.dom.root.parentNode.removeChild(this.dom.root);
- }
- this.dom = null;
-
- // cleanup hammer touch events
- for (var event in this.listeners) {
- if (this.listeners.hasOwnProperty(event)) {
- delete this.listeners[event];
- }
- }
- this.listeners = null;
- this.hammer = null;
-
- // give all components the opportunity to cleanup
- this.components.forEach(function (component) {
- component.destroy();
- });
+ Graph3d.prototype.setSize = function(width, height) {
+ this.frame.style.width = width;
+ this.frame.style.height = height;
- this.body = null;
+ this._resizeCanvas();
};
/**
- * Set options. Options will be passed to all components loaded in the Graph2d.
- * @param {Object} [options]
- * {String} orientation
- * Vertical orientation for the Graph2d,
- * can be 'bottom' (default) or 'top'.
- * {String | Number} width
- * Width for the timeline, a number in pixels or
- * a css string like '1000px' or '75%'. '100%' by default.
- * {String | Number} height
- * Fixed height for the Graph2d, a number in pixels or
- * a css string like '400px' or '75%'. If undefined,
- * The Graph2d will automatically size such that
- * its contents fit.
- * {String | Number} minHeight
- * Minimum height for the Graph2d, a number in pixels or
- * a css string like '400px' or '75%'.
- * {String | Number} maxHeight
- * Maximum height for the Graph2d, a number in pixels or
- * a css string like '400px' or '75%'.
- * {Number | Date | String} start
- * Start date for the visible window
- * {Number | Date | String} end
- * End date for the visible window
+ * Resize the canvas to the current size of the frame
*/
- Graph2d.prototype.setOptions = function (options) {
- if (options) {
- // copy the known options
- var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
- util.selectiveExtend(fields, this.options, options);
+ Graph3d.prototype._resizeCanvas = function() {
+ this.frame.canvas.style.width = '100%';
+ this.frame.canvas.style.height = '100%';
- // enable/disable autoResize
- this._initAutoResize();
- }
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
- // propagate options to all components
- this.components.forEach(function (component) {
- component.setOptions(options);
- });
+ // adjust with for margin
+ this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
+ };
- // TODO: remove deprecation error one day (deprecated since version 0.8.0)
- if (options && options.order) {
- throw new Error('Option order is deprecated. There is no replacement for this feature.');
- }
+ /**
+ * Start animation
+ */
+ Graph3d.prototype.animationStart = function() {
+ if (!this.frame.filter || !this.frame.filter.slider)
+ throw 'No animation available';
- // redraw everything
- this.redraw();
+ this.frame.filter.slider.play();
};
+
/**
- * Set a custom time bar
- * @param {Date} time
+ * Stop animation
*/
- Graph2d.prototype.setCustomTime = function (time) {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
- }
+ Graph3d.prototype.animationStop = function() {
+ if (!this.frame.filter || !this.frame.filter.slider) return;
- this.customTime.setCustomTime(time);
+ this.frame.filter.slider.stop();
};
+
/**
- * Retrieve the current custom time.
- * @return {Date} customTime
+ * Resize the center position based on the current values in this.defaultXCenter
+ * and this.defaultYCenter (which are strings with a percentage or a value
+ * in pixels). The center positions are the variables this.xCenter
+ * and this.yCenter
*/
- Graph2d.prototype.getCustomTime = function() {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ Graph3d.prototype._resizeCenter = function() {
+ // calculate the horizontal center position
+ if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
+ this.xcenter =
+ parseFloat(this.defaultXCenter) / 100 *
+ this.frame.canvas.clientWidth;
+ }
+ else {
+ this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
}
- return this.customTime.getCustomTime();
+ // calculate the vertical center position
+ if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
+ this.ycenter =
+ parseFloat(this.defaultYCenter) / 100 *
+ (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
+ }
+ else {
+ this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
+ }
};
/**
- * Set items
- * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
+ * Set the rotation and distance of the camera
+ * @param {Object} pos An object with the camera position. The object
+ * contains three parameters:
+ * - horizontal {Number}
+ * The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * - vertical {Number}
+ * The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
+ * - distance {Number}
+ * The (normalized) distance of the camera to the
+ * center of the graph, a value between 0.71 and 5.0.
+ * Optional, can be left undefined.
*/
- Graph2d.prototype.setItems = function(items) {
- var initialLoad = (this.itemsData == null);
-
- // convert to type DataSet when needed
- var newDataSet;
- if (!items) {
- newDataSet = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- newDataSet = items;
+ Graph3d.prototype.setCameraPosition = function(pos) {
+ if (pos === undefined) {
+ return;
}
- else {
- // turn an array into a dataset
- newDataSet = new DataSet(items, {
- type: {
- start: 'Date',
- end: 'Date'
- }
- });
+
+ if (pos.horizontal !== undefined && pos.vertical !== undefined) {
+ this.camera.setArmRotation(pos.horizontal, pos.vertical);
}
- // set items
- this.itemsData = newDataSet;
- this.linegraph && this.linegraph.setItems(newDataSet);
+ if (pos.distance !== undefined) {
+ this.camera.setArmLength(pos.distance);
+ }
- if (initialLoad && ('start' in this.options || 'end' in this.options)) {
- this.fit();
+ this.redraw();
+ };
- var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
- var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
- this.setWindow(start, end);
- }
+ /**
+ * Retrieve the current camera rotation
+ * @return {object} An object with parameters horizontal, vertical, and
+ * distance
+ */
+ Graph3d.prototype.getCameraPosition = function() {
+ var pos = this.camera.getArmRotation();
+ pos.distance = this.camera.getArmLength();
+ return pos;
};
/**
- * Set groups
- * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ * Load data into the 3D Graph
*/
- Graph2d.prototype.setGroups = function(groups) {
- // convert to type DataSet when needed
- var newDataSet;
- if (!groups) {
- newDataSet = null;
- }
- else if (groups instanceof DataSet || groups instanceof DataView) {
- newDataSet = groups;
+ Graph3d.prototype._readData = function(data) {
+ // read the data
+ this._dataInitialize(data, this.style);
+
+
+ if (this.dataFilter) {
+ // apply filtering
+ this.dataPoints = this.dataFilter._getDataPoints();
}
else {
- // turn an array into a dataset
- newDataSet = new DataSet(groups);
+ // no filtering. load all data
+ this.dataPoints = this._getDataPoints(this.dataTable);
}
- this.groupsData = newDataSet;
- this.linegraph.setGroups(newDataSet);
+ // draw the filter
+ this._redrawFilter();
};
/**
- * Clear the Graph2d. By Default, items, groups and options are cleared.
- * Example usage:
- *
- * timeline.clear(); // clear items, groups, and options
- * timeline.clear({options: true}); // clear options only
- *
- * @param {Object} [what] Optionally specify what to clear. By default:
- * {items: true, groups: true, options: true}
+ * Replace the dataset of the Graph3d
+ * @param {Array | DataSet | DataView} data
*/
- Graph2d.prototype.clear = function(what) {
- // clear items
- if (!what || what.items) {
- this.setItems(null);
- }
-
- // clear groups
- if (!what || what.groups) {
- this.setGroups(null);
- }
-
- // clear options of timeline and of each of the components
- if (!what || what.options) {
- this.components.forEach(function (component) {
- component.setOptions(component.defaultOptions);
- });
+ Graph3d.prototype.setData = function (data) {
+ this._readData(data);
+ this.redraw();
- this.setOptions(this.defaultOptions); // this will also do a redraw
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
}
};
/**
- * Set Graph2d window such that it fits all items
+ * Update the options. Options will be merged with current options
+ * @param {Object} options
*/
- Graph2d.prototype.fit = function() {
- // apply the data range as range
- var dataRange = this.getItemRange();
+ Graph3d.prototype.setOptions = function (options) {
+ var cameraPosition = undefined;
- // add 5% space on both sides
- var start = dataRange.min;
- var end = dataRange.max;
- if (start != null && end != null) {
- var interval = (end.valueOf() - start.valueOf());
- if (interval <= 0) {
- // prevent an empty interval
- interval = 24 * 60 * 60 * 1000; // 1 day
+ this.animationStop();
+
+ if (options !== undefined) {
+ // retrieve parameter values
+ if (options.width !== undefined) this.width = options.width;
+ if (options.height !== undefined) this.height = options.height;
+
+ if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
+ if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
+
+ if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
+ if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
+ if (options.xLabel !== undefined) this.xLabel = options.xLabel;
+ if (options.yLabel !== undefined) this.yLabel = options.yLabel;
+ if (options.zLabel !== undefined) this.zLabel = options.zLabel;
+
+ if (options.style !== undefined) {
+ var styleNumber = this._getStyleNumber(options.style);
+ if (styleNumber !== -1) {
+ this.style = styleNumber;
+ }
}
- start = new Date(start.valueOf() - interval * 0.05);
- end = new Date(end.valueOf() + interval * 0.05);
- }
+ if (options.showGrid !== undefined) this.showGrid = options.showGrid;
+ if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
+ if (options.showShadow !== undefined) this.showShadow = options.showShadow;
+ if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
+ if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
+ if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
+ if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
- // skip range set if there is no start and end date
- if (start === null && end === null) {
- return;
- }
+ if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
+ if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
+ if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
- this.range.setRange(start, end);
- };
+ if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
+ if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
- /**
- * Get the data range of the item set.
- * @returns {{min: Date, max: Date}} range A range with a start and end Date.
- * When no minimum is found, min==null
- * When no maximum is found, max==null
- */
- Graph2d.prototype.getItemRange = function() {
- // calculate min from start filed
- var itemsData = this.itemsData,
- min = null,
- max = null;
+ if (options.xMin !== undefined) this.defaultXMin = options.xMin;
+ if (options.xStep !== undefined) this.defaultXStep = options.xStep;
+ if (options.xMax !== undefined) this.defaultXMax = options.xMax;
+ if (options.yMin !== undefined) this.defaultYMin = options.yMin;
+ if (options.yStep !== undefined) this.defaultYStep = options.yStep;
+ if (options.yMax !== undefined) this.defaultYMax = options.yMax;
+ if (options.zMin !== undefined) this.defaultZMin = options.zMin;
+ if (options.zStep !== undefined) this.defaultZStep = options.zStep;
+ if (options.zMax !== undefined) this.defaultZMax = options.zMax;
+ if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
+ if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
- if (itemsData) {
- // calculate the minimum value of the field 'start'
- var minItem = itemsData.min('start');
- min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
- // Note: we convert first to Date and then to number because else
- // a conversion from ISODate to Number will fail
+ if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
- // calculate maximum value of fields 'start' and 'end'
- var maxStartItem = itemsData.max('start');
- if (maxStartItem) {
- max = util.convert(maxStartItem.start, 'Date').valueOf();
+ if (cameraPosition !== undefined) {
+ this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
+ this.camera.setArmLength(cameraPosition.distance);
}
- var maxEndItem = itemsData.max('end');
- if (maxEndItem) {
- if (max == null) {
- max = util.convert(maxEndItem.end, 'Date').valueOf();
- }
- else {
- max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
- }
+ else {
+ this.camera.setArmRotation(1.0, 0.5);
+ this.camera.setArmLength(1.7);
}
}
- return {
- min: (min != null) ? new Date(min) : null,
- max: (max != null) ? new Date(max) : null
- };
+ this._setBackgroundColor(options && options.backgroundColor);
+
+ this.setSize(this.width, this.height);
+
+ // re-load the data
+ if (this.dataTable) {
+ this.setData(this.dataTable);
+ }
+
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
+ }
};
/**
- * Set the visible window. Both parameters are optional, you can change only
- * start or only end. Syntax:
- *
- * TimeLine.setWindow(start, end)
- * TimeLine.setWindow(range)
- *
- * Where start and end can be a Date, number, or string, and range is an
- * object with properties start and end.
- *
- * @param {Date | Number | String | Object} [start] Start date of visible window
- * @param {Date | Number | String} [end] End date of visible window
+ * Redraw the Graph.
*/
- Graph2d.prototype.setWindow = function(start, end) {
- if (arguments.length == 1) {
- var range = arguments[0];
- this.range.setRange(range.start, range.end);
+ Graph3d.prototype.redraw = function() {
+ if (this.dataPoints === undefined) {
+ throw 'Error: graph data not initialized';
+ }
+
+ this._resizeCanvas();
+ this._resizeCenter();
+ this._redrawSlider();
+ this._redrawClear();
+ this._redrawAxis();
+
+ if (this.style === Graph3d.STYLE.GRID ||
+ this.style === Graph3d.STYLE.SURFACE) {
+ this._redrawDataGrid();
+ }
+ else if (this.style === Graph3d.STYLE.LINE) {
+ this._redrawDataLine();
+ }
+ else if (this.style === Graph3d.STYLE.BAR ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ this._redrawDataBar();
}
else {
- this.range.setRange(start, end);
+ // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
+ this._redrawDataDot();
}
+
+ this._redrawInfo();
+ this._redrawLegend();
};
/**
- * Get the visible window
- * @return {{start: Date, end: Date}} Visible range
+ * Clear the canvas before redrawing
*/
- Graph2d.prototype.getWindow = function() {
- var range = this.range.getRange();
- return {
- start: new Date(range.start),
- end: new Date(range.end)
- };
+ Graph3d.prototype._redrawClear = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
};
+
/**
- * Force a redraw of the Graph2d. Can be useful to manually redraw when
- * option autoResize=false
+ * Redraw the legend showing the colors
*/
- Graph2d.prototype.redraw = function() {
- var resized = false,
- options = this.options,
- props = this.props,
- dom = this.dom;
-
- if (!dom) return; // when destroyed
+ Graph3d.prototype._redrawLegend = function() {
+ var y;
- // update class names
- dom.root.className = 'vis timeline root ' + options.orientation;
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE) {
- // update root width and height options
- dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
- dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
- dom.root.style.width = util.option.asSize(options.width, '');
+ var dotSize = this.frame.clientWidth * 0.02;
- // calculate border widths
- props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
- props.border.right = props.border.left;
- props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
- props.border.bottom = props.border.top;
- var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
- var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
+ var widthMin, widthMax;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ widthMin = dotSize / 2; // px
+ widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
+ }
+ else {
+ widthMin = 20; // px
+ widthMax = 20; // px
+ }
- // calculate the heights. If any of the side panels is empty, we set the height to
- // minus the border width, such that the border will be invisible
- props.center.height = dom.center.offsetHeight;
- props.left.height = dom.left.offsetHeight;
- props.right.height = dom.right.offsetHeight;
- props.top.height = dom.top.clientHeight || -props.border.top;
- props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+ var height = Math.max(this.frame.clientHeight * 0.25, 100);
+ var top = this.margin;
+ var right = this.frame.clientWidth - this.margin;
+ var left = right - widthMax;
+ var bottom = top + height;
+ }
- // TODO: compensate borders when any of the panels is empty.
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ ctx.lineWidth = 1;
+ ctx.font = '14px arial'; // TODO: put in options
- // apply auto height
- // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
- var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
- var autoHeight = props.top.height + contentHeight + props.bottom.height +
- borderRootHeight + props.border.top + props.border.bottom;
- dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+ if (this.style === Graph3d.STYLE.DOTCOLOR) {
+ // draw the color bar
+ var ymin = 0;
+ var ymax = height; // Todo: make height customizable
+ for (y = ymin; y < ymax; y++) {
+ var f = (y - ymin) / (ymax - ymin);
- // calculate heights of the content panels
- props.root.height = dom.root.offsetHeight;
- props.background.height = props.root.height - borderRootHeight;
- var containerHeight = props.root.height - props.top.height - props.bottom.height -
- borderRootHeight;
- props.centerContainer.height = containerHeight;
- props.leftContainer.height = containerHeight;
- props.rightContainer.height = props.leftContainer.height;
+ //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
+ var hue = f * 240;
+ var color = this._hsv2rgb(hue, 1, 1);
- // calculate the widths of the panels
- props.root.width = dom.root.offsetWidth;
- props.background.width = props.root.width - borderRootWidth;
- props.left.width = dom.leftContainer.clientWidth || -props.border.left;
- props.leftContainer.width = props.left.width;
- props.right.width = dom.rightContainer.clientWidth || -props.border.right;
- props.rightContainer.width = props.right.width;
- var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
- props.center.width = centerWidth;
- props.centerContainer.width = centerWidth;
- props.top.width = centerWidth;
- props.bottom.width = centerWidth;
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(left, top + y);
+ ctx.lineTo(right, top + y);
+ ctx.stroke();
+ }
- // resize the panels
- dom.background.style.height = props.background.height + 'px';
- dom.backgroundVertical.style.height = props.background.height + 'px';
- dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
- dom.centerContainer.style.height = props.centerContainer.height + 'px';
- dom.leftContainer.style.height = props.leftContainer.height + 'px';
- dom.rightContainer.style.height = props.rightContainer.height + 'px';
+ ctx.strokeStyle = this.colorAxis;
+ ctx.strokeRect(left, top, widthMax, height);
+ }
- dom.background.style.width = props.background.width + 'px';
- dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
- dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
- dom.backgroundHorizontal.style.width = props.background.width + 'px';
- dom.centerContainer.style.width = props.center.width + 'px';
- dom.top.style.width = props.top.width + 'px';
- dom.bottom.style.width = props.bottom.width + 'px';
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ // draw border around color bar
+ ctx.strokeStyle = this.colorAxis;
+ ctx.fillStyle = this.colorDot;
+ ctx.beginPath();
+ ctx.moveTo(left, top);
+ ctx.lineTo(right, top);
+ ctx.lineTo(right - widthMax + widthMin, bottom);
+ ctx.lineTo(left, bottom);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
- // reposition the panels
- dom.background.style.left = '0';
- dom.background.style.top = '0';
- dom.backgroundVertical.style.left = props.left.width + 'px';
- dom.backgroundVertical.style.top = '0';
- dom.backgroundHorizontalContainer.style.left = '0';
- dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
- dom.centerContainer.style.left = props.left.width + 'px';
- dom.centerContainer.style.top = props.top.height + 'px';
- dom.leftContainer.style.left = '0';
- dom.leftContainer.style.top = props.top.height + 'px';
- dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
- dom.rightContainer.style.top = props.top.height + 'px';
- dom.top.style.left = props.left.width + 'px';
- dom.top.style.top = '0';
- dom.bottom.style.left = props.left.width + 'px';
- dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
+ if (this.style === Graph3d.STYLE.DOTCOLOR ||
+ this.style === Graph3d.STYLE.DOTSIZE) {
+ // print values along the color bar
+ var gridLineLen = 5; // px
+ var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
+ step.start();
+ if (step.getCurrent() < this.valueMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
- // update the scrollTop, feasible range for the offset can be changed
- // when the height of the Graph2d or of the contents of the center changed
- this._updateScrollTop();
+ ctx.beginPath();
+ ctx.moveTo(left - gridLineLen, y);
+ ctx.lineTo(left, y);
+ ctx.stroke();
- // reposition the scrollable contents
- var offset = this.props.scrollTop;
- if (options.orientation == 'bottom') {
- offset += Math.max(this.props.centerContainer.height - this.props.center.height -
- this.props.border.top - this.props.border.bottom, 0);
- }
- dom.center.style.left = '0';
- dom.center.style.top = offset + 'px';
- dom.backgroundHorizontal.style.left = '0';
- dom.backgroundHorizontal.style.top = offset + 'px';
- dom.left.style.left = '0';
- dom.left.style.top = offset + 'px';
- dom.right.style.left = '0';
- dom.right.style.top = offset + 'px';
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
- // show shadows when vertical scrolling is available
- var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
- var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
- dom.shadowTop.style.visibility = visibilityTop;
- dom.shadowBottom.style.visibility = visibilityBottom;
- dom.shadowTopLeft.style.visibility = visibilityTop;
- dom.shadowBottomLeft.style.visibility = visibilityBottom;
- dom.shadowTopRight.style.visibility = visibilityTop;
- dom.shadowBottomRight.style.visibility = visibilityBottom;
+ step.next();
+ }
- // redraw all components
- this.components.forEach(function (component) {
- resized = component.redraw() || resized;
- });
- if (resized) {
- // keep redrawing until all sizes are settled
- this.redraw();
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'top';
+ var label = this.legendLabel;
+ ctx.fillText(label, right, bottom + this.margin);
}
};
/**
- * Convert a position on screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- * @private
+ * Redraw the filter
*/
- // TODO: move this function to Range
- Graph2d.prototype._toTime = function(x) {
- var conversion = this.range.conversion(this.props.center.width);
- return new Date(x / conversion.scale + conversion.offset);
- };
+ Graph3d.prototype._redrawFilter = function() {
+ this.frame.filter.innerHTML = '';
- /**
- * Convert a datetime (Date object) into a position on the root
- * This is used to get the pixel density estimate for the screen, not the center panel
- * @param {Date} time A date
- * @return {int} x The position on root in pixels which corresponds
- * with the given date.
- * @private
- */
- // TODO: move this function to Range
- Graph2d.prototype._toGlobalTime = function(x) {
- var conversion = this.range.conversion(this.props.root.width);
- return new Date(x / conversion.scale + conversion.offset);
- };
+ if (this.dataFilter) {
+ var options = {
+ 'visible': this.showAnimationControls
+ };
+ var slider = new Slider(this.frame.filter, options);
+ this.frame.filter.slider = slider;
- /**
- * Convert a datetime (Date object) into a position on the screen
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
- * @private
- */
- // TODO: move this function to Range
- Graph2d.prototype._toScreen = function(time) {
- var conversion = this.range.conversion(this.props.center.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
- };
+ // TODO: css here is not nice here...
+ this.frame.filter.style.padding = '10px';
+ //this.frame.filter.style.backgroundColor = '#EFEFEF';
+ slider.setValues(this.dataFilter.values);
+ slider.setPlayInterval(this.animationInterval);
- /**
- * Convert a datetime (Date object) into a position on the root
- * This is used to get the pixel density estimate for the screen, not the center panel
- * @param {Date} time A date
- * @return {int} x The position on root in pixels which corresponds
- * with the given date.
- * @private
- */
- // TODO: move this function to Range
- Graph2d.prototype._toGlobalScreen = function(time) {
- var conversion = this.range.conversion(this.props.root.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
- };
+ // create an event handler
+ var me = this;
+ var onchange = function () {
+ var index = slider.getIndex();
- /**
- * Initialize watching when option autoResize is true
- * @private
- */
- Graph2d.prototype._initAutoResize = function () {
- if (this.options.autoResize == true) {
- this._startAutoResize();
+ me.dataFilter.selectValue(index);
+ me.dataPoints = me.dataFilter._getDataPoints();
+
+ me.redraw();
+ };
+ slider.setOnChangeCallback(onchange);
}
else {
- this._stopAutoResize();
+ this.frame.filter.slider = undefined;
}
};
/**
- * Watch for changes in the size of the container. On resize, the Panel will
- * automatically redraw itself.
- * @private
+ * Redraw the slider
*/
- Graph2d.prototype._startAutoResize = function () {
- var me = this;
-
- this._stopAutoResize();
-
- this._onResize = function() {
- if (me.options.autoResize != true) {
- // stop watching when the option autoResize is changed to false
- me._stopAutoResize();
- return;
- }
+ Graph3d.prototype._redrawSlider = function() {
+ if ( this.frame.filter.slider !== undefined) {
+ this.frame.filter.slider.redraw();
+ }
+ };
- if (me.dom.root) {
- // check whether the frame is resized
- if ((me.dom.root.clientWidth != me.props.lastWidth) ||
- (me.dom.root.clientHeight != me.props.lastHeight)) {
- me.props.lastWidth = me.dom.root.clientWidth;
- me.props.lastHeight = me.dom.root.clientHeight;
- me.emit('change');
- }
- }
- };
+ /**
+ * Redraw common information
+ */
+ Graph3d.prototype._redrawInfo = function() {
+ if (this.dataFilter) {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
- // add event listener to window resize
- util.addEventListener(window, 'resize', this._onResize);
+ ctx.font = '14px arial'; // TODO: put in options
+ ctx.lineStyle = 'gray';
+ ctx.fillStyle = 'gray';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
- this.watchTimer = setInterval(this._onResize, 1000);
+ var x = this.margin;
+ var y = this.margin;
+ ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
+ }
};
+
/**
- * Stop watching for a resize of the frame.
- * @private
+ * Redraw the axis
*/
- Graph2d.prototype._stopAutoResize = function () {
- if (this.watchTimer) {
- clearInterval(this.watchTimer);
- this.watchTimer = undefined;
- }
+ Graph3d.prototype._redrawAxis = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ from, to, step, prettyStep,
+ text, xText, yText, zText,
+ offset, xOffset, yOffset,
+ xMin2d, xMax2d;
- // remove event listener on window.resize
- util.removeEventListener(window, 'resize', this._onResize);
- this._onResize = null;
- };
+ // TODO: get the actual rendered style of the containerElement
+ //ctx.font = this.containerElement.style.font;
+ ctx.font = 24 / this.camera.getArmLength() + 'px arial';
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Graph2d.prototype._onTouch = function (event) {
- this.touch.allowDragging = true;
- };
+ // calculate the length for the short grid lines
+ var gridLenX = 0.025 / this.scale.x;
+ var gridLenY = 0.025 / this.scale.y;
+ var textMargin = 5 / this.camera.getArmLength(); // px
+ var armAngle = this.camera.getArmRotation().horizontal;
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Graph2d.prototype._onPinch = function (event) {
- this.touch.allowDragging = false;
- };
+ // draw x-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultXStep === undefined);
+ step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.xMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ var x = step.getCurrent();
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Graph2d.prototype._onDragStart = function (event) {
- this.touch.initialScrollTop = this.props.scrollTop;
- };
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+ else {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
- /**
- * Move the timeline vertically
- * @param {Event} event
- * @private
- */
- Graph2d.prototype._onDrag = function (event) {
- // 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
- if (!this.touch.allowDragging) return;
+ from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
- var delta = event.gesture.deltaY;
+ yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
+ text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
+ }
+ else if (Math.sin(armAngle * 2) < 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
- var oldScrollTop = this._getScrollTop();
- var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+ step.next();
+ }
- if (newScrollTop != oldScrollTop) {
- this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
+ // draw y-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultYStep === undefined);
+ step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.yMin) {
+ step.next();
}
- };
+ while (!step.end()) {
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+ else {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
- /**
- * Apply a scrollTop
- * @param {Number} scrollTop
- * @returns {Number} scrollTop Returns the applied scrollTop
- * @private
- */
- Graph2d.prototype._setScrollTop = function (scrollTop) {
- this.props.scrollTop = scrollTop;
- this._updateScrollTop();
- return this.props.scrollTop;
- };
+ from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
- /**
- * Update the current scrollTop when the height of the containers has been changed
- * @returns {Number} scrollTop Returns the applied scrollTop
- * @private
- */
- Graph2d.prototype._updateScrollTop = function () {
- // recalculate the scrollTopMin
- var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
- if (scrollTopMin != this.props.scrollTopMin) {
- // in case of bottom orientation, change the scrollTop such that the contents
- // do not move relative to the time axis at the bottom
- if (this.options.orientation == 'bottom') {
- this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
+ text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
}
- this.props.scrollTopMin = scrollTopMin;
+ else if (Math.sin(armAngle * 2) > 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
+
+ step.next();
}
- // limit the scrollTop to the feasible scroll range
- if (this.props.scrollTop > 0) this.props.scrollTop = 0;
- if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+ // draw z-grid lines and axis
+ ctx.lineWidth = 1;
+ prettyStep = (this.defaultZStep === undefined);
+ step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.zMin) {
+ step.next();
+ }
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
+ while (!step.end()) {
+ // TODO: make z-grid lines really 3d?
+ from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(from.x - textMargin, from.y);
+ ctx.stroke();
- return this.props.scrollTop;
- };
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
- /**
- * Get the current scrollTop
- * @returns {number} scrollTop
- * @private
- */
- Graph2d.prototype._getScrollTop = function () {
- return this.props.scrollTop;
- };
+ step.next();
+ }
+ ctx.lineWidth = 1;
+ from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
- module.exports = Graph2d;
-
-
-/***/ },
-/* 8 */
-/***/ function(module, exports, __webpack_require__) {
-
- /**
- * @constructor DataStep
- * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
- * end data point. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
- *
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
- */
- function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
- // variables
- this.current = 0;
-
- this.autoScale = true;
- this.stepIndex = 0;
- this.step = 1;
- this.scale = 1;
-
- this.marginStart;
- this.marginEnd;
-
- this.majorSteps = [1, 2, 5, 10];
- this.minorSteps = [0.25, 0.5, 1, 2];
-
- this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
- }
+ // draw x-axis
+ ctx.lineWidth = 1;
+ // line at yMin
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+ // line at ymax
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+ // draw y-axis
+ ctx.lineWidth = 1;
+ // line at xMin
+ from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ // line at xMax
+ from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.colorAxis;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ // draw x-label
+ var xLabel = this.xLabel;
+ if (xLabel.length > 0) {
+ yOffset = 0.1 / this.scale.y;
+ xText = (this.xMin + this.xMax) / 2;
+ yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ }
+ else if (Math.sin(armAngle * 2) < 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(xLabel, text.x, text.y);
+ }
- /**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Number} [start] The start date and time.
- * @param {Number} [end] The end date and time.
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
- */
- DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
- this._start = start;
- this._end = end;
+ // draw y-label
+ var yLabel = this.yLabel;
+ if (yLabel.length > 0) {
+ xOffset = 0.1 / this.scale.x;
+ xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
+ yText = (this.yMin + this.yMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ }
+ else if (Math.sin(armAngle * 2) > 0){
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ }
+ else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(yLabel, text.x, text.y);
+ }
- if (this.autoScale) {
- this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
+ // draw z-label
+ var zLabel = this.zLabel;
+ if (zLabel.length > 0) {
+ offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
+ xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
+ yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
+ zText = (this.zMin + this.zMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, zText));
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.colorAxis;
+ ctx.fillText(zLabel, text.x - offset, text.y);
}
- this.setFirst();
};
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Calculate the color based on the given value.
+ * @param {Number} H Hue, a value be between 0 and 360
+ * @param {Number} S Saturation, a value between 0 and 1
+ * @param {Number} V Value, a value between 0 and 1
*/
- DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
- // round to floor
- var size = this._end - this._start;
- var safeSize = size * 1.1;
- var minimumStepValue = minimumStep * (safeSize / containerHeight);
- var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+ Graph3d.prototype._hsv2rgb = function(H, S, V) {
+ var R, G, B, C, Hi, X;
- var minorStepIdx = -1;
- var magnitudefactor = Math.pow(10,orderOfMagnitude);
+ C = V * S;
+ Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
+ X = C * (1 - Math.abs(((H/60) % 2) - 1));
- var start = 0;
- if (orderOfMagnitude < 0) {
- start = orderOfMagnitude;
- }
+ switch (Hi) {
+ case 0: R = C; G = X; B = 0; break;
+ case 1: R = X; G = C; B = 0; break;
+ case 2: R = 0; G = C; B = X; break;
+ case 3: R = 0; G = X; B = C; break;
+ case 4: R = X; G = 0; B = C; break;
+ case 5: R = C; G = 0; B = X; break;
- var solutionFound = false;
- for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
- magnitudefactor = Math.pow(10,i);
- for (var j = 0; j < this.minorSteps.length; j++) {
- var stepSize = magnitudefactor * this.minorSteps[j];
- if (stepSize >= minimumStepValue) {
- solutionFound = true;
- minorStepIdx = j;
- break;
- }
- }
- if (solutionFound == true) {
- break;
- }
+ default: R = 0; G = 0; B = 0; break;
}
- this.stepIndex = minorStepIdx;
- this.scale = magnitudefactor;
- this.step = magnitudefactor * this.minorSteps[minorStepIdx];
+
+ return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
};
/**
- * Set the range iterator to the start date.
+ * Draw all datapoints as a grid
+ * This function can be used when the style is 'grid'
*/
- DataStep.prototype.first = function() {
- this.setFirst();
- };
+ Graph3d.prototype._redrawDataGrid = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point, right, top, cross,
+ i,
+ topSideVisible, fillStyle, strokeStyle, lineWidth,
+ h, s, v, zAvg;
- /**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
- */
- DataStep.prototype.setFirst = function() {
- var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
- var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
- this.marginEnd = this.roundToMinor(niceEnd);
- this.marginStart = this.roundToMinor(niceStart);
- this.marginRange = this.marginEnd - this.marginStart;
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
- this.current = this.marginEnd;
+ // calculate the translations and screen position of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
- };
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
- DataStep.prototype.roundToMinor = function(value) {
- var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
- if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
- return rounded + (this.scale * this.minorSteps[this.stepIndex]);
- }
- else {
- return rounded;
+ // calculate the translation of the point at the bottom (needed for sorting)
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
}
- }
+ // sort the points on depth of their (x,y) position (not on z)
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
- /**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
- */
- DataStep.prototype.hasNext = function () {
- return (this.current >= this.marginStart);
- };
+ if (this.style === Graph3d.STYLE.SURFACE) {
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
+ cross = this.dataPoints[i].pointCross;
- /**
- * Do the next step
- */
- DataStep.prototype.next = function() {
- var prev = this.current;
- this.current -= this.step;
+ if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current == prev) {
- this.current = this._end;
- }
- };
+ if (this.showGrayBottom || this.showShadow) {
+ // calculate the cross product of the two vectors from center
+ // to left and right, in order to know whether we are looking at the
+ // bottom or at the top side. We can also use the cross product
+ // for calculating light intensity
+ var aDiff = Point3d.subtract(cross.trans, point.trans);
+ var bDiff = Point3d.subtract(top.trans, right.trans);
+ var crossproduct = Point3d.crossProduct(aDiff, bDiff);
+ var len = crossproduct.length();
+ // FIXME: there is a bug with determining the surface side (shadow or colored)
- /**
- * Do the next step
- */
- DataStep.prototype.previous = function() {
- this.current += this.step;
- this.marginEnd += this.step;
- this.marginRange = this.marginEnd - this.marginStart;
- };
+ topSideVisible = (crossproduct.z > 0);
+ }
+ else {
+ topSideVisible = true;
+ }
+ if (topSideVisible) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ s = 1; // saturation
+ if (this.showShadow) {
+ v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = fillStyle;
+ }
+ else {
+ v = 1;
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = this.colorAxis;
+ }
+ }
+ else {
+ fillStyle = 'gray';
+ strokeStyle = this.colorAxis;
+ }
+ lineWidth = 0.5;
- /**
- * Get the current datetime
- * @return {String} current The current date
- */
- DataStep.prototype.getCurrent = function() {
- var toPrecision = '' + Number(this.current).toPrecision(5);
- for (var i = toPrecision.length-1; i > 0; i--) {
- if (toPrecision[i] == "0") {
- toPrecision = toPrecision.slice(0,i);
- }
- else if (toPrecision[i] == "." || toPrecision[i] == ",") {
- toPrecision = toPrecision.slice(0,i);
- break;
- }
- else{
- break;
+ ctx.lineWidth = lineWidth;
+ ctx.fillStyle = fillStyle;
+ ctx.strokeStyle = strokeStyle;
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.lineTo(cross.screen.x, cross.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
}
}
+ else { // grid style
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
- return toPrecision;
- };
+ if (point !== undefined) {
+ if (this.showPerspective) {
+ lineWidth = 2 / -point.trans.z;
+ }
+ else {
+ lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
+ }
+ }
+ if (point !== undefined && right !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.stroke();
+ }
- /**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
- */
- DataStep.prototype.snap = function(date) {
+ if (point !== undefined && top !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + top.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.stroke();
+ }
+ }
+ }
};
+
/**
- * Check if the current value is a major value (for example when the step
- * is DAY, a major value is each first day of the MONTH)
- * @return {boolean} true if current date is major, else false.
+ * Draw all datapoints as dots.
+ * This function can be used when the style is 'dot' or 'dot-line'
*/
- DataStep.prototype.isMajor = function() {
- return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
- };
-
- module.exports = DataStep;
-
-
-/***/ },
-/* 9 */
-/***/ function(module, exports, __webpack_require__) {
+ Graph3d.prototype._redrawDataDot = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i;
- var util = __webpack_require__(1);
- var moment = __webpack_require__(39);
- var Component = __webpack_require__(12);
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
- /**
- * @constructor Range
- * A Range controls a numeric range with a start and end value.
- * The Range adjusts the range based on mouse events or programmatic changes,
- * and triggers events when the range is changing or has been changed.
- * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
- * @param {Object} [options] See description at Range.setOptions
- */
- function Range(body, options) {
- var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
- this.start = now.clone().add('days', -3).valueOf(); // Number
- this.end = now.clone().add('days', 4).valueOf(); // Number
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
- this.body = body;
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
- // default options
- this.defaultOptions = {
- start: null,
- end: null,
- direction: 'horizontal', // 'horizontal' or 'vertical'
- moveable: true,
- zoomable: true,
- min: null,
- max: null,
- zoomMin: 10, // milliseconds
- zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
+ // order the translated points by depth
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
};
- this.options = util.extend({}, this.defaultOptions);
+ this.dataPoints.sort(sortDepth);
- this.props = {
- touch: {}
- };
+ // draw the datapoints as colored circles
+ var dotSize = this.frame.clientWidth * 0.02; // px
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
- // drag listeners for dragging
- this.body.emitter.on('dragstart', this._onDragStart.bind(this));
- this.body.emitter.on('drag', this._onDrag.bind(this));
- this.body.emitter.on('dragend', this._onDragEnd.bind(this));
+ if (this.style === Graph3d.STYLE.DOTLINE) {
+ // draw a vertical line from the bottom to the graph value
+ //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
+ var from = this._convert3Dto2D(point.bottom);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = this.colorGrid;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(point.screen.x, point.screen.y);
+ ctx.stroke();
+ }
- // ignore dragging when holding
- this.body.emitter.on('hold', this._onHold.bind(this));
+ // calculate radius for the circle
+ var size;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
+ }
+ else {
+ size = dotSize;
+ }
- // mouse wheel for zooming
- this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
- this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
+ var radius;
+ if (this.showPerspective) {
+ radius = size / -point.trans.z;
+ }
+ else {
+ radius = size * -(this.eye.z / this.camera.getArmLength());
+ }
+ if (radius < 0) {
+ radius = 0;
+ }
- // pinch to zoom
- this.body.emitter.on('touch', this._onTouch.bind(this));
- this.body.emitter.on('pinch', this._onPinch.bind(this));
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.DOTCOLOR ) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+ else if (this.style === Graph3d.STYLE.DOTSIZE) {
+ color = this.colorDot;
+ borderColor = this.colorDotBorder;
+ }
+ else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
- this.setOptions(options);
- }
-
- Range.prototype = new Component();
-
- /**
- * Set options for the range controller
- * @param {Object} options Available options:
- * {Number | Date | String} start Start date for the range
- * {Number | Date | String} end End date for the range
- * {Number} min Minimum value for start
- * {Number} max Maximum value for end
- * {Number} zoomMin Set a minimum value for
- * (end - start).
- * {Number} zoomMax Set a maximum value for
- * (end - start).
- * {Boolean} moveable Enable moving of the range
- * by dragging. True by default
- * {Boolean} zoomable Enable zooming of the range
- * by pinching/scrolling. True by default
- */
- Range.prototype.setOptions = function (options) {
- if (options) {
- // copy the options that we know
- var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
- util.selectiveExtend(fields, this.options, options);
-
- if ('start' in options || 'end' in options) {
- // apply a new range. both start and end are optional
- this.setRange(options.start, options.end);
- }
+ // draw the circle
+ ctx.lineWidth = 1.0;
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
+ ctx.fill();
+ ctx.stroke();
}
};
/**
- * Test whether direction has a valid value
- * @param {String} direction 'horizontal' or 'vertical'
- */
- function validateDirection (direction) {
- if (direction != 'horizontal' && direction != 'vertical') {
- throw new TypeError('Unknown direction "' + direction + '". ' +
- 'Choose "horizontal" or "vertical".');
- }
- }
-
- /**
- * Set a new start and end range
- * @param {Number} [start]
- * @param {Number} [end]
+ * Draw all datapoints as bars.
+ * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
*/
- Range.prototype.setRange = function(start, end) {
- var changed = this._applyRange(start, end);
- if (changed) {
- var params = {
- start: new Date(this.start),
- end: new Date(this.end)
- };
- this.body.emitter.emit('rangechange', params);
- this.body.emitter.emit('rangechanged', params);
- }
- };
+ Graph3d.prototype._redrawDataBar = function() {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i, j, surface, corners;
- /**
- * Set a new start and end range. This method is the same as setRange, but
- * does not trigger a range change and range changed event, and it returns
- * true when the range is changed
- * @param {Number} [start]
- * @param {Number} [end]
- * @return {Boolean} changed
- * @private
- */
- Range.prototype._applyRange = function(start, end) {
- var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
- newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
- max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
- min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
- diff;
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
- // check for valid number
- if (isNaN(newStart) || newStart === null) {
- throw new Error('Invalid start "' + start + '"');
- }
- if (isNaN(newEnd) || newEnd === null) {
- throw new Error('Invalid end "' + end + '"');
- }
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
- // prevent start < end
- if (newEnd < newStart) {
- newEnd = newStart;
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
}
- // prevent start < min
- if (min !== null) {
- if (newStart < min) {
- diff = (min - newStart);
- newStart += diff;
- newEnd += diff;
-
- // prevent end > max
- if (max != null) {
- if (newEnd > max) {
- newEnd = max;
- }
- }
- }
- }
+ // order the translated points by depth
+ var sortDepth = function (a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
- // prevent end > max
- if (max !== null) {
- if (newEnd > max) {
- diff = (newEnd - max);
- newStart -= diff;
- newEnd -= diff;
+ // draw the datapoints as bars
+ var xWidth = this.xBarWidth / 2;
+ var yWidth = this.yBarWidth / 2;
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
- // prevent start < min
- if (min != null) {
- if (newStart < min) {
- newStart = min;
- }
- }
+ // determine color
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.BARCOLOR ) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
}
- }
-
- // prevent (end-start) < zoomMin
- if (this.options.zoomMin !== null) {
- var zoomMin = parseFloat(this.options.zoomMin);
- if (zoomMin < 0) {
- zoomMin = 0;
+ else if (this.style === Graph3d.STYLE.BARSIZE) {
+ color = this.colorDot;
+ borderColor = this.colorDotBorder;
}
- if ((newEnd - newStart) < zoomMin) {
- if ((this.end - this.start) === zoomMin) {
- // ignore this action, we are already zoomed to the minimum
- newStart = this.start;
- newEnd = this.end;
- }
- else {
- // zoom to the minimum
- diff = (zoomMin - (newEnd - newStart));
- newStart -= diff / 2;
- newEnd += diff / 2;
- }
+ else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
}
- }
- // prevent (end-start) > zoomMax
- if (this.options.zoomMax !== null) {
- var zoomMax = parseFloat(this.options.zoomMax);
- if (zoomMax < 0) {
- zoomMax = 0;
- }
- if ((newEnd - newStart) > zoomMax) {
- if ((this.end - this.start) === zoomMax) {
- // ignore this action, we are already zoomed to the maximum
- newStart = this.start;
- newEnd = this.end;
- }
- else {
- // zoom to the maximum
- diff = ((newEnd - newStart) - zoomMax);
- newStart += diff / 2;
- newEnd -= diff / 2;
- }
+ // calculate size for the bar
+ if (this.style === Graph3d.STYLE.BARSIZE) {
+ xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+ yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
}
- }
- var changed = (this.start != newStart || this.end != newEnd);
+ // calculate all corner points
+ var me = this;
+ var point3d = point.point;
+ var top = [
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
+ ];
+ var bottom = [
+ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
+ {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
+ {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
+ ];
- this.start = newStart;
- this.end = newEnd;
+ // calculate screen location of the points
+ top.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
+ bottom.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
- return changed;
- };
+ // create five sides, calculate both corner points and center points
+ var surfaces = [
+ {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
+ {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
+ {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
+ {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
+ {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
+ ];
+ point.surfaces = surfaces;
- /**
- * Retrieve the current range.
- * @return {Object} An object with start and end properties
- */
- Range.prototype.getRange = function() {
- return {
- start: this.start,
- end: this.end
- };
- };
+ // calculate the distance of each of the surface centers to the camera
+ for (j = 0; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ var transCenter = this._convertPointToTranslation(surface.center);
+ surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
+ // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
+ // but the current solution is fast/simple and works in 99.9% of all cases
+ // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
+ }
- /**
- * Calculate the conversion offset and scale for current range, based on
- * the provided width
- * @param {Number} width
- * @returns {{offset: number, scale: number}} conversion
- */
- Range.prototype.conversion = function (width) {
- return Range.conversion(this.start, this.end, width);
- };
+ // order the surfaces by their (translated) depth
+ surfaces.sort(function (a, b) {
+ var diff = b.dist - a.dist;
+ if (diff) return diff;
- /**
- * Static method to calculate the conversion offset and scale for a range,
- * based on the provided start, end, and width
- * @param {Number} start
- * @param {Number} end
- * @param {Number} width
- * @returns {{offset: number, scale: number}} conversion
- */
- Range.conversion = function (start, end, width) {
- if (width != 0 && (end - start != 0)) {
- return {
- offset: start,
- scale: width / (end - start)
+ // if equal depth, sort the top surface last
+ if (a.corners === top) return 1;
+ if (b.corners === top) return -1;
+
+ // both are equal
+ return 0;
+ });
+
+ // draw the ordered surfaces
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
+ for (j = 2; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ corners = surface.corners;
+ ctx.beginPath();
+ ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
+ ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
+ ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
+ ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.fill();
+ ctx.stroke();
}
}
- else {
- return {
- offset: 0,
- scale: 1
- };
- }
};
+
/**
- * Start dragging horizontally or vertically
- * @param {Event} event
- * @private
+ * Draw a line through all datapoints.
+ * This function can be used when the style is 'line'
*/
- Range.prototype._onDragStart = function(event) {
- // only allow dragging when configured as movable
- if (!this.options.moveable) return;
+ Graph3d.prototype._redrawDataLine = function() {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point, i;
- // 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
- if (!this.props.touch.allowDragging) return;
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0)
+ return; // TODO: throw exception?
- this.props.touch.start = this.start;
- this.props.touch.end = this.end;
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
- if (this.body.dom.root) {
- this.body.dom.root.style.cursor = 'move';
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
}
- };
-
- /**
- * Perform dragging operation
- * @param {Event} event
- * @private
- */
- Range.prototype._onDrag = function (event) {
- // only allow dragging when configured as movable
- if (!this.options.moveable) return;
- var direction = this.options.direction;
- validateDirection(direction);
- // 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
- if (!this.props.touch.allowDragging) return;
- var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
- interval = (this.props.touch.end - this.props.touch.start),
- width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
- diffRange = -delta / width * interval;
- this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
- this.body.emitter.emit('rangechange', {
- start: new Date(this.start),
- end: new Date(this.end)
- });
- };
- /**
- * Stop dragging operation
- * @param {event} event
- * @private
- */
- Range.prototype._onDragEnd = function (event) {
- // only allow dragging when configured as movable
- if (!this.options.moveable) return;
+ // start the line
+ if (this.dataPoints.length > 0) {
+ point = this.dataPoints[0];
- // 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
- if (!this.props.touch.allowDragging) return;
+ ctx.lineWidth = 1; // TODO: make customizable
+ ctx.strokeStyle = 'blue'; // TODO: make customizable
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ }
- if (this.body.dom.root) {
- this.body.dom.root.style.cursor = 'auto';
+ // draw the datapoints as colored circles
+ for (i = 1; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ ctx.lineTo(point.screen.x, point.screen.y);
}
- // fire a rangechanged event
- this.body.emitter.emit('rangechanged', {
- start: new Date(this.start),
- end: new Date(this.end)
- });
+ // finish the line
+ if (this.dataPoints.length > 0) {
+ ctx.stroke();
+ }
};
/**
- * Event handler for mouse wheel event, used to zoom
- * Code from http://adomas.org/javascript-mouse-wheel/
- * @param {Event} event
- * @private
+ * Start a moving operation inside the provided parent element
+ * @param {Event} event The event that occurred (required for
+ * retrieving the mouse position)
*/
- Range.prototype._onMouseWheel = function(event) {
- // only allow zooming when configured as zoomable and moveable
- if (!(this.options.zoomable && this.options.moveable)) return;
+ Graph3d.prototype._onMouseDown = function(event) {
+ event = event || window.event;
- // retrieve delta
- var delta = 0;
- if (event.wheelDelta) { /* IE/Opera. */
- delta = event.wheelDelta / 120;
- } else if (event.detail) { /* Mozilla case. */
- // In Mozilla, sign of delta is different than in IE.
- // Also, delta is multiple of 3.
- delta = -event.detail / 3;
+ // check if mouse is still down (may be up when focus is lost for example
+ // in an iframe)
+ if (this.leftButtonDown) {
+ this._onMouseUp(event);
}
- // If delta is nonzero, handle it.
- // Basically, delta is now positive if wheel was scrolled up,
- // and negative, if wheel was scrolled down.
- if (delta) {
- // perform the zoom action. Delta is normally 1 or -1
-
- // adjust a negative delta such that zooming in with delta 0.1
- // equals zooming out with a delta -0.1
- var scale;
- if (delta < 0) {
- scale = 1 - (delta / 5);
- }
- else {
- scale = 1 / (1 + (delta / 5)) ;
- }
+ // only react on left mouse button down
+ this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
+ if (!this.leftButtonDown && !this.touchDown) return;
- // calculate center, the date to zoom around
- var gesture = util.fakeGesture(this, event),
- pointer = getPointer(gesture.center, this.body.dom.center),
- pointerDate = this._pointerToDate(pointer);
+ // get mouse position (different code for IE and all other browsers)
+ this.startMouseX = getMouseX(event);
+ this.startMouseY = getMouseY(event);
- this.zoom(scale, pointerDate);
- }
+ this.startStart = new Date(this.start);
+ this.startEnd = new Date(this.end);
+ this.startArmRotation = this.camera.getArmRotation();
- // Prevent default actions caused by mouse wheel
- // (else the page and timeline both zoom and scroll)
- event.preventDefault();
- };
+ this.frame.style.cursor = 'move';
- /**
- * Start of a touch gesture
- * @private
- */
- Range.prototype._onTouch = function (event) {
- this.props.touch.start = this.start;
- this.props.touch.end = this.end;
- this.props.touch.allowDragging = true;
- this.props.touch.center = null;
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {me._onMouseMove(event);};
+ this.onmouseup = function (event) {me._onMouseUp(event);};
+ G3DaddEventListener(document, 'mousemove', me.onmousemove);
+ G3DaddEventListener(document, 'mouseup', me.onmouseup);
+ G3DpreventDefault(event);
};
- /**
- * On start of a hold gesture
- * @private
- */
- Range.prototype._onHold = function () {
- this.props.touch.allowDragging = false;
- };
/**
- * Handle pinch event
- * @param {Event} event
- * @private
+ * Perform moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {Event} event Well, eehh, the event
*/
- Range.prototype._onPinch = function (event) {
- // only allow zooming when configured as zoomable and moveable
- if (!(this.options.zoomable && this.options.moveable)) return;
-
- this.props.touch.allowDragging = false;
+ Graph3d.prototype._onMouseMove = function (event) {
+ event = event || window.event;
- if (event.gesture.touches.length > 1) {
- if (!this.props.touch.center) {
- this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
- }
+ // calculate change in mouse position
+ var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
+ var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
- var scale = 1 / event.gesture.scale,
- initDate = this._pointerToDate(this.props.touch.center);
+ var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
+ var verticalNew = this.startArmRotation.vertical + diffY / 200;
- // calculate new start and end
- var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
- var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
+ var snapAngle = 4; // degrees
+ var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
- // apply new range
- this.setRange(newStart, newEnd);
+ // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
+ // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
+ if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
+ horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
+ }
+ if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
+ horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
}
- };
-
- /**
- * Helper function to calculate the center date for zooming
- * @param {{x: Number, y: Number}} pointer
- * @return {number} date
- * @private
- */
- Range.prototype._pointerToDate = function (pointer) {
- var conversion;
- var direction = this.options.direction;
-
- validateDirection(direction);
- if (direction == 'horizontal') {
- var width = this.body.domProps.center.width;
- conversion = this.conversion(width);
- return pointer.x / conversion.scale + conversion.offset;
+ // snap vertically to nice angles
+ if (Math.abs(Math.sin(verticalNew)) < snapValue) {
+ verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
}
- else {
- var height = this.body.domProps.center.height;
- conversion = this.conversion(height);
- return pointer.y / conversion.scale + conversion.offset;
+ if (Math.abs(Math.cos(verticalNew)) < snapValue) {
+ verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
}
+
+ this.camera.setArmRotation(horizontalNew, verticalNew);
+ this.redraw();
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ G3DpreventDefault(event);
};
- /**
- * Get the pointer location relative to the location of the dom element
- * @param {{pageX: Number, pageY: Number}} touch
- * @param {Element} element HTML DOM element
- * @return {{x: Number, y: Number}} pointer
- * @private
- */
- function getPointer (touch, element) {
- return {
- x: touch.pageX - util.getAbsoluteLeft(element),
- y: touch.pageY - util.getAbsoluteTop(element)
- };
- }
/**
- * Zoom the range the given scale in or out. Start and end date will
- * be adjusted, and the timeline will be redrawn. You can optionally give a
- * date around which to zoom.
- * For example, try scale = 0.9 or 1.1
- * @param {Number} scale Scaling factor. Values above 1 will zoom out,
- * values below 1 will zoom in.
- * @param {Number} [center] Value representing a date around which will
- * be zoomed.
+ * Stop moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {event} event The event
*/
- Range.prototype.zoom = function(scale, center) {
- // if centerDate is not provided, take it half between start Date and end Date
- if (center == null) {
- center = (this.start + this.end) / 2;
- }
-
- // calculate new start and end
- var newStart = center + (this.start - center) * scale;
- var newEnd = center + (this.end - center) * scale;
+ Graph3d.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+ this.leftButtonDown = false;
- this.setRange(newStart, newEnd);
+ // remove event listeners here
+ G3DremoveEventListener(document, 'mousemove', this.onmousemove);
+ G3DremoveEventListener(document, 'mouseup', this.onmouseup);
+ G3DpreventDefault(event);
};
/**
- * Move the range with a given delta to the left or right. Start and end
- * value will be adjusted. For example, try delta = 0.1 or -0.1
- * @param {Number} delta Moving amount. Positive value will move right,
- * negative value will move left
+ * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
+ * @param {Event} event A mouse move event
*/
- Range.prototype.move = function(delta) {
- // zoom start Date and end Date relative to the centerDate
- var diff = (this.end - this.start);
+ Graph3d.prototype._onTooltip = function (event) {
+ var delay = 300; // ms
+ var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
+ var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
- // apply new values
- var newStart = this.start + diff * delta;
- var newEnd = this.end + diff * delta;
+ if (!this.showTooltip) {
+ return;
+ }
- // TODO: reckon with min and max range
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ }
- this.start = newStart;
- this.end = newEnd;
+ // (delayed) display of a tooltip only if no mouse button is down
+ if (this.leftButtonDown) {
+ this._hideTooltip();
+ return;
+ }
+
+ if (this.tooltip && this.tooltip.dataPoint) {
+ // tooltip is currently visible
+ var dataPoint = this._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint !== this.tooltip.dataPoint) {
+ // datapoint changed
+ if (dataPoint) {
+ this._showTooltip(dataPoint);
+ }
+ else {
+ this._hideTooltip();
+ }
+ }
+ }
+ else {
+ // tooltip is currently not visible
+ var me = this;
+ this.tooltipTimeout = setTimeout(function () {
+ me.tooltipTimeout = null;
+
+ // show a tooltip if we have a data point
+ var dataPoint = me._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint) {
+ me._showTooltip(dataPoint);
+ }
+ }, delay);
+ }
};
/**
- * Move the range to a new center point
- * @param {Number} moveTo New center point of the range
+ * Event handler for touchstart event on mobile devices
*/
- Range.prototype.moveTo = function(moveTo) {
- var center = (this.start + this.end) / 2;
-
- var diff = center - moveTo;
+ Graph3d.prototype._onTouchStart = function(event) {
+ this.touchDown = true;
- // calculate new start and end
- var newStart = this.start - diff;
- var newEnd = this.end - diff;
+ var me = this;
+ this.ontouchmove = function (event) {me._onTouchMove(event);};
+ this.ontouchend = function (event) {me._onTouchEnd(event);};
+ G3DaddEventListener(document, 'touchmove', me.ontouchmove);
+ G3DaddEventListener(document, 'touchend', me.ontouchend);
- this.setRange(newStart, newEnd);
+ this._onMouseDown(event);
};
- module.exports = Range;
-
-
-/***/ },
-/* 10 */
-/***/ function(module, exports, __webpack_require__) {
-
- // Utility functions for ordering and stacking of items
- var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
-
/**
- * Order items by their start data
- * @param {Item[]} items
+ * Event handler for touchmove event on mobile devices
*/
- exports.orderByStart = function(items) {
- items.sort(function (a, b) {
- return a.data.start - b.data.start;
- });
+ Graph3d.prototype._onTouchMove = function(event) {
+ this._onMouseMove(event);
};
/**
- * Order items by their end date. If they have no end date, their start date
- * is used.
- * @param {Item[]} items
+ * Event handler for touchend event on mobile devices
*/
- exports.orderByEnd = function(items) {
- items.sort(function (a, b) {
- var aTime = ('end' in a.data) ? a.data.end : a.data.start,
- bTime = ('end' in b.data) ? b.data.end : b.data.start;
+ Graph3d.prototype._onTouchEnd = function(event) {
+ this.touchDown = false;
- return aTime - bTime;
- });
+ G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
+ G3DremoveEventListener(document, 'touchend', this.ontouchend);
+
+ this._onMouseUp(event);
};
+
/**
- * Adjust vertical positions of the items such that they don't overlap each
- * other.
- * @param {Item[]} items
- * All visible items
- * @param {{item: number, axis: number}} margin
- * Margins between items and between items and the axis.
- * @param {boolean} [force=false]
- * If true, all items will be repositioned. If false (default), only
- * items having a top===null will be re-stacked
+ * Event handler for mouse wheel event, used to zoom the graph
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {event} event The event
*/
- exports.stack = function(items, margin, force) {
- var i, iMax;
+ Graph3d.prototype._onWheel = function(event) {
+ if (!event) /* For IE. */
+ event = window.event;
- if (force) {
- // reset top position of all items
- for (i = 0, iMax = items.length; i < iMax; i++) {
- items[i].top = null;
- }
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta/120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail/3;
}
- // calculate new, non-overlapping positions
- for (i = 0, iMax = items.length; i < iMax; i++) {
- var item = items[i];
- if (item.top === null) {
- // initialize top position
- item.top = margin.axis;
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ var oldLength = this.camera.getArmLength();
+ var newLength = oldLength * (1 - delta / 10);
- do {
- // TODO: optimize checking for overlap. when there is a gap without items,
- // you only need to check for items from the next item on, not from zero
- var collidingItem = null;
- for (var j = 0, jj = items.length; j < jj; j++) {
- var other = items[j];
- if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) {
- collidingItem = other;
- break;
- }
- }
+ this.camera.setArmLength(newLength);
+ this.redraw();
- if (collidingItem != null) {
- // There is a collision. Reposition the items above the colliding element
- item.top = collidingItem.top + collidingItem.height + margin.item;
- }
- } while (collidingItem);
- }
+ this._hideTooltip();
}
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ // Prevent default actions caused by mouse wheel.
+ // That might be ugly, but we handle scrolls somehow
+ // anyway, so don't bother here..
+ G3DpreventDefault(event);
};
/**
- * Adjust vertical positions of the items without stacking them
- * @param {Item[]} items
- * All visible items
- * @param {{item: number, axis: number}} margin
- * Margins between items and between items and the axis.
+ * Test whether a point lies inside given 2D triangle
+ * @param {Point2d} point
+ * @param {Point2d[]} triangle
+ * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
+ * @private
*/
- exports.nostack = function(items, margin) {
- var i, iMax;
+ Graph3d.prototype._insideTriangle = function (point, triangle) {
+ var a = triangle[0],
+ b = triangle[1],
+ c = triangle[2];
- // reset top position of all items
- for (i = 0, iMax = items.length; i < iMax; i++) {
- items[i].top = margin.axis;
+ function sign (x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
}
+
+ var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
+ var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
+ var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
+
+ // each of the three signs must be either equal to each other or zero
+ return (as == 0 || bs == 0 || as == bs) &&
+ (bs == 0 || cs == 0 || bs == cs) &&
+ (as == 0 || cs == 0 || as == cs);
};
/**
- * Test if the two provided items collide
- * The items must have parameters left, width, top, and height.
- * @param {Item} a The first item
- * @param {Item} b The second item
- * @param {Number} margin A minimum required margin.
- * If margin is provided, the two items will be
- * marked colliding when they overlap or
- * when the margin between the two is smaller than
- * the requested margin.
- * @return {boolean} true if a and b collide, else false
+ * Find a data point close to given screen position (x, y)
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Object | null} The closest data point or null if not close to any data point
+ * @private
*/
- exports.collision = function(a, b, margin) {
- return ((a.left - margin + EPSILON) < (b.left + b.width) &&
- (a.left + a.width + margin - EPSILON) > b.left &&
- (a.top - margin + EPSILON) < (b.top + b.height) &&
- (a.top + a.height + margin - EPSILON) > b.top);
- };
+ Graph3d.prototype._dataPointFromXY = function (x, y) {
+ var i,
+ distMax = 100, // px
+ dataPoint = null,
+ closestDataPoint = null,
+ closestDist = null,
+ center = new Point2d(x, y);
+
+ if (this.style === Graph3d.STYLE.BAR ||
+ this.style === Graph3d.STYLE.BARCOLOR ||
+ this.style === Graph3d.STYLE.BARSIZE) {
+ // the data points are ordered from far away to closest
+ for (i = this.dataPoints.length - 1; i >= 0; i--) {
+ dataPoint = this.dataPoints[i];
+ var surfaces = dataPoint.surfaces;
+ if (surfaces) {
+ for (var s = surfaces.length - 1; s >= 0; s--) {
+ // split each surface in two triangles, and see if the center point is inside one of these
+ var surface = surfaces[s];
+ var corners = surface.corners;
+ var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
+ var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
+ if (this._insideTriangle(center, triangle1) ||
+ this._insideTriangle(center, triangle2)) {
+ // return immediately at the first hit
+ return dataPoint;
+ }
+ }
+ }
+ }
+ }
+ else {
+ // find the closest data point, using distance to the center of the point on 2d screen
+ for (i = 0; i < this.dataPoints.length; i++) {
+ dataPoint = this.dataPoints[i];
+ var point = dataPoint.screen;
+ if (point) {
+ var distX = Math.abs(x - point.x);
+ var distY = Math.abs(y - point.y);
+ var dist = Math.sqrt(distX * distX + distY * distY);
+ if ((closestDist === null || dist < closestDist) && dist < distMax) {
+ closestDist = dist;
+ closestDataPoint = dataPoint;
+ }
+ }
+ }
+ }
-/***/ },
-/* 11 */
-/***/ function(module, exports, __webpack_require__) {
- var moment = __webpack_require__(39);
+ return closestDataPoint;
+ };
/**
- * @constructor TimeStep
- * The class TimeStep is an iterator for dates. You provide a start date and an
- * end date. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
- *
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ * Display a tooltip for given data point
+ * @param {Object} dataPoint
+ * @private
*/
- function TimeStep(start, end, minimumStep) {
- // variables
- this.current = new Date();
- this._start = new Date();
- this._end = new Date();
-
- this.autoScale = true;
- this.scale = TimeStep.SCALE.DAY;
- this.step = 1;
+ Graph3d.prototype._showTooltip = function (dataPoint) {
+ var content, line, dot;
- // initialize the range
- this.setRange(start, end, minimumStep);
- }
+ if (!this.tooltip) {
+ content = document.createElement('div');
+ content.style.position = 'absolute';
+ content.style.padding = '10px';
+ content.style.border = '1px solid #4d4d4d';
+ content.style.color = '#1a1a1a';
+ content.style.background = 'rgba(255,255,255,0.7)';
+ content.style.borderRadius = '2px';
+ content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
- /// enum scale
- TimeStep.SCALE = {
- MILLISECOND: 1,
- SECOND: 2,
- MINUTE: 3,
- HOUR: 4,
- DAY: 5,
- WEEKDAY: 6,
- MONTH: 7,
- YEAR: 8
- };
+ line = document.createElement('div');
+ line.style.position = 'absolute';
+ line.style.height = '40px';
+ line.style.width = '0';
+ line.style.borderLeft = '1px solid #4d4d4d';
+ dot = document.createElement('div');
+ dot.style.position = 'absolute';
+ dot.style.height = '0';
+ dot.style.width = '0';
+ dot.style.border = '5px solid #4d4d4d';
+ dot.style.borderRadius = '5px';
- /**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Date} [start] The start date and time.
- * @param {Date} [end] The end date and time.
- * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
- */
- TimeStep.prototype.setRange = function(start, end, minimumStep) {
- if (!(start instanceof Date) || !(end instanceof Date)) {
- throw "No legal start or end date in method setRange";
+ this.tooltip = {
+ dataPoint: null,
+ dom: {
+ content: content,
+ line: line,
+ dot: dot
+ }
+ };
+ }
+ else {
+ content = this.tooltip.dom.content;
+ line = this.tooltip.dom.line;
+ dot = this.tooltip.dom.dot;
}
- this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
- this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
+ this._hideTooltip();
- if (this.autoScale) {
- this.setMinimumStep(minimumStep);
+ this.tooltip.dataPoint = dataPoint;
+ if (typeof this.showTooltip === 'function') {
+ content.innerHTML = this.showTooltip(dataPoint.point);
+ }
+ else {
+ content.innerHTML = '' +
+ 'x: | ' + dataPoint.point.x + ' |
' +
+ 'y: | ' + dataPoint.point.y + ' |
' +
+ 'z: | ' + dataPoint.point.z + ' |
' +
+ '
';
}
- };
- /**
- * Set the range iterator to the start date.
- */
- TimeStep.prototype.first = function() {
- this.current = new Date(this._start.valueOf());
- this.roundToMinor();
+ content.style.left = '0';
+ content.style.top = '0';
+ this.frame.appendChild(content);
+ this.frame.appendChild(line);
+ this.frame.appendChild(dot);
+
+ // calculate sizes
+ var contentWidth = content.offsetWidth;
+ var contentHeight = content.offsetHeight;
+ var lineHeight = line.offsetHeight;
+ var dotWidth = dot.offsetWidth;
+ var dotHeight = dot.offsetHeight;
+
+ var left = dataPoint.screen.x - contentWidth / 2;
+ left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
+
+ line.style.left = dataPoint.screen.x + 'px';
+ line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
+ content.style.left = left + 'px';
+ content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
+ dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
+ dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
};
/**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
+ * Hide the tooltip when displayed
+ * @private
*/
- TimeStep.prototype.roundToMinor = function() {
- // round to floor
- // IMPORTANT: we have no breaks in this switch! (this is no bug)
- //noinspection FallthroughInSwitchStatementJS
- switch (this.scale) {
- case TimeStep.SCALE.YEAR:
- this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
- this.current.setMonth(0);
- case TimeStep.SCALE.MONTH: this.current.setDate(1);
- case TimeStep.SCALE.DAY: // intentional fall through
- case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
- case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
- case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
- case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
- //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
- }
+ Graph3d.prototype._hideTooltip = function () {
+ if (this.tooltip) {
+ this.tooltip.dataPoint = null;
- if (this.step != 1) {
- // round down to the first minor value that is a multiple of the current step size
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
- case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
- case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
- case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
- case TimeStep.SCALE.WEEKDAY: // intentional fall through
- case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
- case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
- case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
- default: break;
+ for (var prop in this.tooltip.dom) {
+ if (this.tooltip.dom.hasOwnProperty(prop)) {
+ var elem = this.tooltip.dom[prop];
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
}
}
};
+
/**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example 'click',
+ * without the prefix 'on'
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} useCapture
*/
- TimeStep.prototype.hasNext = function () {
- return (this.current.valueOf() <= this._end.valueOf());
+ G3DaddEventListener = function(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+ action = 'DOMMouseScroll'; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent('on' + action, listener); // IE browsers
+ }
};
/**
- * Do the next step
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example 'mousedown'
+ * @param {function} listener The listener function
+ * @param {boolean} useCapture
*/
- TimeStep.prototype.next = function() {
- var prev = this.current.valueOf();
-
- // Two cases, needed to prevent issues with switching daylight savings
- // (end of March and end of October)
- if (this.current.getMonth() < 6) {
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND:
+ G3DremoveEventListener = function(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
- this.current = new Date(this.current.valueOf() + this.step); break;
- case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
- case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
- case TimeStep.SCALE.HOUR:
- this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
- // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
- var h = this.current.getHours();
- this.current.setHours(h - (h % this.step));
- break;
- case TimeStep.SCALE.WEEKDAY: // intentional fall through
- case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
- case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
- case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
- default: break;
- }
- }
- else {
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
- case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
- case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
- case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
- case TimeStep.SCALE.WEEKDAY: // intentional fall through
- case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
- case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
- case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
- default: break;
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+ action = 'DOMMouseScroll'; // For Firefox
}
- }
- if (this.step != 1) {
- // round down to the correct major value
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
- case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
- case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
- case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
- case TimeStep.SCALE.WEEKDAY: // intentional fall through
- case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
- case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
- case TimeStep.SCALE.YEAR: break; // nothing to do for year
- default: break;
- }
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent('on' + action, listener);
}
+ };
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current.valueOf() == prev) {
- this.current = new Date(this._end.valueOf());
+ /**
+ * Stop event propagation
+ */
+ G3DstopPropagation = function(event) {
+ if (!event)
+ event = window.event;
+
+ if (event.stopPropagation) {
+ event.stopPropagation(); // non-IE browsers
+ }
+ else {
+ event.cancelBubble = true; // IE browsers
}
};
/**
- * Get the current datetime
- * @return {Date} current The current date
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
*/
- TimeStep.prototype.getCurrent = function() {
- return this.current;
+ G3DpreventDefault = function (event) {
+ if (!event)
+ event = window.event;
+
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
+ }
+ else {
+ event.returnValue = false; // IE browsers
+ }
};
/**
- * Set a custom scale. Autoscaling will be disabled.
- * For example setScale(SCALE.MINUTES, 5) will result
- * in minor steps of 5 minutes, and major steps of an hour.
+ * @constructor Slider
*
- * @param {TimeStep.SCALE} newScale
- * A scale. Choose from SCALE.MILLISECOND,
- * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
- * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
- * SCALE.YEAR.
- * @param {Number} newStep A step size, by default 1. Choose for
- * example 1, 2, 5, or 10.
+ * An html slider control with start/stop/prev/next buttons
+ * @param {Element} container The element where the slider will be created
+ * @param {Object} options Available options:
+ * {boolean} visible If true (default) the
+ * slider is visible.
*/
- TimeStep.prototype.setScale = function(newScale, newStep) {
- this.scale = newScale;
+ function Slider(container, options) {
+ if (container === undefined) {
+ throw 'Error: No container element defined';
+ }
+ this.container = container;
+ this.visible = (options && options.visible != undefined) ? options.visible : true;
- if (newStep > 0) {
- this.step = newStep;
+ if (this.visible) {
+ this.frame = document.createElement('DIV');
+ //this.frame.style.backgroundColor = '#E5E5E5';
+ this.frame.style.width = '100%';
+ this.frame.style.position = 'relative';
+ this.container.appendChild(this.frame);
+
+ this.frame.prev = document.createElement('INPUT');
+ this.frame.prev.type = 'BUTTON';
+ this.frame.prev.value = 'Prev';
+ this.frame.appendChild(this.frame.prev);
+
+ this.frame.play = document.createElement('INPUT');
+ this.frame.play.type = 'BUTTON';
+ this.frame.play.value = 'Play';
+ this.frame.appendChild(this.frame.play);
+
+ this.frame.next = document.createElement('INPUT');
+ this.frame.next.type = 'BUTTON';
+ this.frame.next.value = 'Next';
+ this.frame.appendChild(this.frame.next);
+
+ this.frame.bar = document.createElement('INPUT');
+ this.frame.bar.type = 'BUTTON';
+ this.frame.bar.style.position = 'absolute';
+ this.frame.bar.style.border = '1px solid red';
+ this.frame.bar.style.width = '100px';
+ this.frame.bar.style.height = '6px';
+ this.frame.bar.style.borderRadius = '2px';
+ this.frame.bar.style.MozBorderRadius = '2px';
+ this.frame.bar.style.border = '1px solid #7F7F7F';
+ this.frame.bar.style.backgroundColor = '#E5E5E5';
+ this.frame.appendChild(this.frame.bar);
+
+ this.frame.slide = document.createElement('INPUT');
+ this.frame.slide.type = 'BUTTON';
+ this.frame.slide.style.margin = '0px';
+ this.frame.slide.value = ' ';
+ this.frame.slide.style.position = 'relative';
+ this.frame.slide.style.left = '-100px';
+ this.frame.appendChild(this.frame.slide);
+
+ // create events
+ var me = this;
+ this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
+ this.frame.prev.onclick = function (event) {me.prev(event);};
+ this.frame.play.onclick = function (event) {me.togglePlay(event);};
+ this.frame.next.onclick = function (event) {me.next(event);};
}
- this.autoScale = false;
- };
+ this.onChangeCallback = undefined;
- /**
- * Enable or disable autoscaling
- * @param {boolean} enable If true, autoascaling is set true
- */
- TimeStep.prototype.setAutoScale = function (enable) {
- this.autoScale = enable;
- };
+ this.values = [];
+ this.index = undefined;
+ this.playTimeout = undefined;
+ this.playInterval = 1000; // milliseconds
+ this.playLoop = true;
+ }
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Select the previous index
*/
- TimeStep.prototype.setMinimumStep = function(minimumStep) {
- if (minimumStep == undefined) {
- return;
+ Slider.prototype.prev = function() {
+ var index = this.getIndex();
+ if (index > 0) {
+ index--;
+ this.setIndex(index);
}
-
- var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
- var stepMonth = (1000 * 60 * 60 * 24 * 30);
- var stepDay = (1000 * 60 * 60 * 24);
- var stepHour = (1000 * 60 * 60);
- var stepMinute = (1000 * 60);
- var stepSecond = (1000);
- var stepMillisecond= (1);
-
- // find the smallest step that is larger than the provided minimumStep
- if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
- if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
- if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
- if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
- if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
- if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
- if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
- if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
- if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
- if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
- if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
- if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
- if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
- if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
- if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
- if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
- if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
- if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
- if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
- if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
- if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
- if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
- if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
- if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
- if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
- if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
- if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
- if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
- if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
};
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * Select the next index
*/
- TimeStep.prototype.snap = function(date) {
- var clone = new Date(date.valueOf());
-
- if (this.scale == TimeStep.SCALE.YEAR) {
- var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
- clone.setFullYear(Math.round(year / this.step) * this.step);
- clone.setMonth(0);
- clone.setDate(0);
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
+ Slider.prototype.next = function() {
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
}
- else if (this.scale == TimeStep.SCALE.MONTH) {
- if (clone.getDate() > 15) {
- clone.setDate(1);
- clone.setMonth(clone.getMonth() + 1);
- // important: first set Date to 1, after that change the month.
- }
- else {
- clone.setDate(1);
- }
+ };
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.DAY) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.WEEKDAY) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.HOUR) {
- switch (this.step) {
- case 4:
- clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
- default:
- clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
- }
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- } else if (this.scale == TimeStep.SCALE.MINUTE) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
- clone.setSeconds(0);
- break;
- case 5:
- clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
- default:
- clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
- }
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.SECOND) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
- clone.setMilliseconds(0);
- break;
- case 5:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
- default:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
- }
+ /**
+ * Select the next index
+ */
+ Slider.prototype.playNext = function() {
+ var start = new Date();
+
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
}
- else if (this.scale == TimeStep.SCALE.MILLISECOND) {
- var step = this.step > 5 ? this.step / 2 : 1;
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
+ else if (this.playLoop) {
+ // jump to the start
+ index = 0;
+ this.setIndex(index);
}
-
- return clone;
+
+ var end = new Date();
+ var diff = (end - start);
+
+ // calculate how much time it to to set the index and to execute the callback
+ // function.
+ var interval = Math.max(this.playInterval - diff, 0);
+ // document.title = diff // TODO: cleanup
+
+ var me = this;
+ this.playTimeout = setTimeout(function() {me.playNext();}, interval);
};
/**
- * Check if the current value is a major value (for example when the step
- * is DAY, a major value is each first day of the MONTH)
- * @return {boolean} true if current date is major, else false.
+ * Toggle start or stop playing
*/
- TimeStep.prototype.isMajor = function() {
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND:
- return (this.current.getMilliseconds() == 0);
- case TimeStep.SCALE.SECOND:
- return (this.current.getSeconds() == 0);
- case TimeStep.SCALE.MINUTE:
- return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
- // Note: this is no bug. Major label is equal for both minute and hour scale
- case TimeStep.SCALE.HOUR:
- return (this.current.getHours() == 0);
- case TimeStep.SCALE.WEEKDAY: // intentional fall through
- case TimeStep.SCALE.DAY:
- return (this.current.getDate() == 1);
- case TimeStep.SCALE.MONTH:
- return (this.current.getMonth() == 0);
- case TimeStep.SCALE.YEAR:
- return false;
- default:
- return false;
+ Slider.prototype.togglePlay = function() {
+ if (this.playTimeout === undefined) {
+ this.play();
+ } else {
+ this.stop();
}
};
-
/**
- * Returns formatted text for the minor axislabel, depending on the current
- * date and the scale. For example when scale is MINUTE, the current time is
- * formatted as "hh:mm".
- * @param {Date} [date] custom date. if not provided, current date is taken
+ * Start playing
*/
- TimeStep.prototype.getLabelMinor = function(date) {
- if (date == undefined) {
- date = this.current;
- }
+ Slider.prototype.play = function() {
+ // Test whether already playing
+ if (this.playTimeout) return;
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
- case TimeStep.SCALE.SECOND: return moment(date).format('s');
- case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
- case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
- case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
- case TimeStep.SCALE.DAY: return moment(date).format('D');
- case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
- case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
- default: return '';
+ this.playNext();
+
+ if (this.frame) {
+ this.frame.play.value = 'Stop';
}
};
-
/**
- * Returns formatted text for the major axis label, depending on the current
- * date and the scale. For example when scale is MINUTE, the major scale is
- * hours, and the hour will be formatted as "hh".
- * @param {Date} [date] custom date. if not provided, current date is taken
+ * Stop playing
*/
- TimeStep.prototype.getLabelMajor = function(date) {
- if (date == undefined) {
- date = this.current;
+ Slider.prototype.stop = function() {
+ clearInterval(this.playTimeout);
+ this.playTimeout = undefined;
+
+ if (this.frame) {
+ this.frame.play.value = 'Play';
}
+ };
- //noinspection FallthroughInSwitchStatementJS
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
- case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
- case TimeStep.SCALE.MINUTE:
- case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
- case TimeStep.SCALE.WEEKDAY:
- case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
- case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
- case TimeStep.SCALE.YEAR: return '';
- default: return '';
+ /**
+ * Set a callback function which will be triggered when the value of the
+ * slider bar has changed.
+ */
+ Slider.prototype.setOnChangeCallback = function(callback) {
+ this.onChangeCallback = callback;
+ };
+
+ /**
+ * Set the interval for playing the list
+ * @param {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.setPlayInterval = function(interval) {
+ this.playInterval = interval;
+ };
+
+ /**
+ * Retrieve the current play interval
+ * @return {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.getPlayInterval = function(interval) {
+ return this.playInterval;
+ };
+
+ /**
+ * Set looping on or off
+ * @pararm {boolean} doLoop If true, the slider will jump to the start when
+ * the end is passed, and will jump to the end
+ * when the start is passed.
+ */
+ Slider.prototype.setPlayLoop = function(doLoop) {
+ this.playLoop = doLoop;
+ };
+
+
+ /**
+ * Execute the onchange callback function
+ */
+ Slider.prototype.onChange = function() {
+ if (this.onChangeCallback !== undefined) {
+ this.onChangeCallback();
}
};
- module.exports = TimeStep;
+ /**
+ * redraw the slider on the correct place
+ */
+ Slider.prototype.redraw = function() {
+ if (this.frame) {
+ // resize the bar
+ this.frame.bar.style.top = (this.frame.clientHeight/2 -
+ this.frame.bar.offsetHeight/2) + 'px';
+ this.frame.bar.style.width = (this.frame.clientWidth -
+ this.frame.prev.clientWidth -
+ this.frame.play.clientWidth -
+ this.frame.next.clientWidth - 30) + 'px';
+
+ // position the slider button
+ var left = this.indexToLeft(this.index);
+ this.frame.slide.style.left = (left) + 'px';
+ }
+ };
+
+
+ /**
+ * Set the list with values for the slider
+ * @param {Array} values A javascript array with values (any type)
+ */
+ Slider.prototype.setValues = function(values) {
+ this.values = values;
+
+ if (this.values.length > 0)
+ this.setIndex(0);
+ else
+ this.index = undefined;
+ };
+
+ /**
+ * Select a value by its index
+ * @param {Number} index
+ */
+ Slider.prototype.setIndex = function(index) {
+ if (index < this.values.length) {
+ this.index = index;
+
+ this.redraw();
+ this.onChange();
+ }
+ else {
+ throw 'Error: index out of range';
+ }
+ };
+
+ /**
+ * retrieve the index of the currently selected vaue
+ * @return {Number} index
+ */
+ Slider.prototype.getIndex = function() {
+ return this.index;
+ };
+
+
+ /**
+ * retrieve the currently selected value
+ * @return {*} value
+ */
+ Slider.prototype.get = function() {
+ return this.values[this.index];
+ };
+
+
+ Slider.prototype._onMouseDown = function(event) {
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
+ if (!leftButtonDown) return;
+
+ this.startClientX = event.clientX;
+ this.startSlideX = parseFloat(this.frame.slide.style.left);
+
+ this.frame.style.cursor = 'move';
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {me._onMouseMove(event);};
+ this.onmouseup = function (event) {me._onMouseUp(event);};
+ G3DaddEventListener(document, 'mousemove', this.onmousemove);
+ G3DaddEventListener(document, 'mouseup', this.onmouseup);
+ G3DpreventDefault(event);
+ };
+
+
+ Slider.prototype.leftToIndex = function (left) {
+ var width = parseFloat(this.frame.bar.style.width) -
+ this.frame.slide.clientWidth - 10;
+ var x = left - 3;
+
+ var index = Math.round(x / width * (this.values.length-1));
+ if (index < 0) index = 0;
+ if (index > this.values.length-1) index = this.values.length-1;
+
+ return index;
+ };
+
+ Slider.prototype.indexToLeft = function (index) {
+ var width = parseFloat(this.frame.bar.style.width) -
+ this.frame.slide.clientWidth - 10;
+
+ var x = index / (this.values.length-1) * width;
+ var left = x + 3;
+
+ return left;
+ };
+
+
+
+ Slider.prototype._onMouseMove = function (event) {
+ var diff = event.clientX - this.startClientX;
+ var x = this.startSlideX + diff;
+
+ var index = this.leftToIndex(x);
+
+ this.setIndex(index);
+
+ G3DpreventDefault();
+ };
+
+
+ Slider.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+
+ // remove event listeners
+ G3DremoveEventListener(document, 'mousemove', this.onmousemove);
+ G3DremoveEventListener(document, 'mouseup', this.onmouseup);
+
+ G3DpreventDefault();
+ };
+
+
+
+ /**--------------------------------------------------------------------------**/
+
+
+
+ /**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} left The absolute left position of this element
+ * in the browser page.
+ */
+ getAbsoluteLeft = function(elem) {
+ var left = 0;
+ while( elem !== null ) {
+ left += elem.offsetLeft;
+ left -= elem.scrollLeft;
+ elem = elem.offsetParent;
+ }
+ return left;
+ };
+
+ /**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} top The absolute top position of this element
+ * in the browser page.
+ */
+ getAbsoluteTop = function(elem) {
+ var top = 0;
+ while( elem !== null ) {
+ top += elem.offsetTop;
+ top -= elem.scrollTop;
+ elem = elem.offsetParent;
+ }
+ return top;
+ };
+
+ /**
+ * Get the horizontal mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse x
+ */
+ getMouseX = function(event) {
+ if ('clientX' in event) return event.clientX;
+ return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
+ };
+
+ /**
+ * Get the vertical mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse y
+ */
+ getMouseY = function(event) {
+ if ('clientY' in event) return event.clientY;
+ return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
+ };
+
+ module.exports = Graph3d;
/***/ },
@@ -9041,7 +9040,7 @@ return /******/ (function(modules) { // webpackBootstrap
/* 14 */
/***/ function(module, exports, __webpack_require__) {
- var Hammer = __webpack_require__(49);
+ var Hammer = __webpack_require__(50);
var util = __webpack_require__(1);
var Component = __webpack_require__(12);
@@ -9238,7 +9237,7 @@ return /******/ (function(modules) { // webpackBootstrap
var util = __webpack_require__(1);
var DOMutil = __webpack_require__(2);
var Component = __webpack_require__(12);
- var DataStep = __webpack_require__(8);
+ var DataStep = __webpack_require__(6);
/**
* A horizontal time axis
@@ -9844,7 +9843,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
- var stack = __webpack_require__(10);
+ var stack = __webpack_require__(9);
var ItemRange = __webpack_require__(25);
/**
@@ -10273,7 +10272,7 @@ return /******/ (function(modules) { // webpackBootstrap
/* 18 */
/***/ function(module, exports, __webpack_require__) {
- var Hammer = __webpack_require__(49);
+ var Hammer = __webpack_require__(50);
var util = __webpack_require__(1);
var DataSet = __webpack_require__(3);
var DataView = __webpack_require__(4);
@@ -12879,7 +12878,7 @@ return /******/ (function(modules) { // webpackBootstrap
var util = __webpack_require__(1);
var Component = __webpack_require__(12);
- var TimeStep = __webpack_require__(11);
+ var TimeStep = __webpack_require__(10);
/**
* A horizontal time axis
@@ -13278,7 +13277,7 @@ return /******/ (function(modules) { // webpackBootstrap
/* 22 */
/***/ function(module, exports, __webpack_require__) {
- var Hammer = __webpack_require__(49);
+ var Hammer = __webpack_require__(50);
/**
* @constructor Item
@@ -13879,7 +13878,7 @@ return /******/ (function(modules) { // webpackBootstrap
/* 25 */
/***/ function(module, exports, __webpack_require__) {
- var Hammer = __webpack_require__(49);
+ var Hammer = __webpack_require__(50);
var Item = __webpack_require__(22);
/**
@@ -14177,8 +14176,8 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
var Emitter = __webpack_require__(41);
- var Hammer = __webpack_require__(49);
- var mousetrap = __webpack_require__(42);
+ var Hammer = __webpack_require__(50);
+ var mousetrap = __webpack_require__(48);
var util = __webpack_require__(1);
var DataSet = __webpack_require__(3);
var DataView = __webpack_require__(4);
@@ -14234,9 +14233,9 @@ return /******/ (function(modules) { // webpackBootstrap
// set constant values
this.constants = {
nodes: {
- radiusMin: 5,
- radiusMax: 20,
- radius: 5,
+ radiusMin: 10,
+ radiusMax: 30,
+ radius: 10,
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
@@ -14284,7 +14283,8 @@ return /******/ (function(modules) { // webpackBootstrap
length: 10,
gap: 5,
altLength: undefined
- }
+ },
+ inheritColor: false // to, from, false, true (== from)
},
configurePhysics:false,
physics: {
@@ -14356,7 +14356,13 @@ return /******/ (function(modules) { // webpackBootstrap
direction: "UD" // UD, DU, LR, RL
},
freezeForStabilization: false,
- smoothCurves: true,
+ smoothCurves: {
+ enabled: true,
+ dynamic: true,
+ type: "continuous",
+ roundness: 0.5
+ },
+ dynamicSmoothCurves: true,
maxVelocity: 10,
minVelocity: 0.1, // px/s
stabilizationIterations: 1000, // maximum number of iteration to stabilize
@@ -14391,10 +14397,12 @@ return /******/ (function(modules) { // webpackBootstrap
dragNetwork: true,
dragNodes: true,
zoomable: true,
- hover: false
+ hover: false,
+ hideEdgesOnDrag: false,
+ hideNodesOnDrag: false
};
this.hoverObj = {nodes:{},edges:{}};
-
+ this.controlNodesActive = false;
// Node variables
var network = this;
@@ -14727,7 +14735,6 @@ return /******/ (function(modules) { // webpackBootstrap
if (options.height !== undefined) {this.height = options.height;}
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;}
- if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
@@ -14735,6 +14742,8 @@ return /******/ (function(modules) { // webpackBootstrap
if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
if (options.hover !== undefined) {this.constants.hover = options.hover;}
+ if (options.hideEdgesOnDrag !== undefined) {this.constants.hideEdgesOnDrag = options.hideEdgesOnDrag;}
+ if (options.hideNodesOnDrag !== undefined) {this.constants.hideNodesOnDrag = options.hideNodesOnDrag;}
// TODO: deprecated since version 3.0.0. Cleanup some day
if (options.dragGraph !== undefined) {
@@ -14800,6 +14809,20 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
+ if (options.smoothCurves !== undefined) {
+ if (typeof options.smoothCurves == 'boolean') {
+ this.constants.smoothCurves.enabled = options.smoothCurves;
+ }
+ else {
+ this.constants.smoothCurves.enabled = true;
+ for (prop in options.smoothCurves) {
+ if (options.smoothCurves.hasOwnProperty(prop)) {
+ this.constants.smoothCurves[prop] = options.smoothCurves[prop];
+ }
+ }
+ }
+ }
+
if (options.hierarchicalLayout) {
this.constants.hierarchicalLayout.enabled = true;
for (prop in options.hierarchicalLayout) {
@@ -14871,7 +14894,6 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
-
if (options.edges.color !== undefined) {
if (util.isString(options.edges.color)) {
this.constants.edges.color = {};
@@ -15201,10 +15223,11 @@ return /******/ (function(modules) { // webpackBootstrap
this._setTranslation(
this.drag.translation.x + diffX,
- this.drag.translation.y + diffY);
+ this.drag.translation.y + diffY
+ );
this._redraw();
- this.moving = true;
- this.start();
+ // this.moving = true;
+ // this.start();
}
}
};
@@ -15223,6 +15246,7 @@ return /******/ (function(modules) { // webpackBootstrap
s.node.yFixed = s.yFixed;
});
}
+ this._redraw();
};
/**
@@ -15918,10 +15942,19 @@ return /******/ (function(modules) { // webpackBootstrap
"y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
};
+
this._doInAllSectors("_drawAllSectorNodes",ctx);
- this._doInAllSectors("_drawEdges",ctx);
- this._doInAllSectors("_drawNodes",ctx,false);
- this._doInAllSectors("_drawControlNodes",ctx);
+ if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
+ this._doInAllSectors("_drawEdges",ctx);
+ }
+
+ if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
+ this._doInAllSectors("_drawNodes",ctx,false);
+ }
+
+ if (this.controlNodesActive == true) {
+ this._doInAllSectors("_drawControlNodes",ctx);
+ }
// this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
@@ -16233,7 +16266,7 @@ return /******/ (function(modules) { // webpackBootstrap
this.moving = true;
}
else {
- this.moving = this._isMoving(vminCorrected);
+ this.moving = this._isMoving(vminCorrected) || this.constants.configurePhysics;
}
}
};
@@ -16282,10 +16315,12 @@ return /******/ (function(modules) { // webpackBootstrap
timeRequired = Date.now() - calculationTime;
maxSteps++;
}
+
// start the rendering process
var renderTime = Date.now();
this._redraw();
this.renderTime = Date.now() - renderTime;
+
};
if (typeof window !== 'undefined') {
@@ -16369,8 +16404,7 @@ return /******/ (function(modules) { // webpackBootstrap
if (disableStart === undefined) {
disableStart = true;
}
-
- if (this.constants.smoothCurves == true) {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
this._createBezierNodes();
}
else {
@@ -16398,7 +16432,7 @@ return /******/ (function(modules) { // webpackBootstrap
* @private
*/
Network.prototype._createBezierNodes = function() {
- if (this.constants.smoothCurves == true) {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
var edge = this.edges[edgeId];
@@ -16534,8 +16568,10 @@ return /******/ (function(modules) { // webpackBootstrap
this.customLength = false;
this.selected = false;
this.hover = false;
- this.smooth = constants.smoothCurves;
+ this.smoothCurves = constants.smoothCurves;
+ this.dynamicSmoothCurves = constants.dynamicSmoothCurves;
this.arrowScaleFactor = constants.edges.arrowScaleFactor;
+ this.inheritColor = constants.edges.inheritColor;
this.from = null; // a node
this.to = null; // a node
@@ -16607,6 +16643,8 @@ return /******/ (function(modules) { // webpackBootstrap
// scale the arrow
if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
+ if (properties.inheritColor !== undefined) {this.inheritColor = properties.inheritColor;}
+
// Added to support dashed lines
// David Jordan
// 2012-08-08
@@ -16751,6 +16789,28 @@ return /******/ (function(modules) { // webpackBootstrap
}
};
+ Edge.prototype._getColor = function() {
+ var colorObj = this.color;
+ if (this.inheritColor == "to") {
+ colorObj = {
+ highlight: this.to.color.highlight.border,
+ hover: this.to.color.hover.border,
+ color: this.to.color.border
+ };
+ }
+ else if (this.inheritColor == "from" || this.inheritColor == true) {
+ colorObj = {
+ highlight: this.from.color.highlight.border,
+ hover: this.from.color.hover.border,
+ color: this.from.color.border
+ };
+ }
+
+ if (this.selected == true) {return colorObj.highlight;}
+ else if (this.hover == true) {return colorObj.hover;}
+ else {return colorObj.color;}
+ }
+
/**
* Redraw a edge as a line
@@ -16761,21 +16821,19 @@ return /******/ (function(modules) { // webpackBootstrap
*/
Edge.prototype._drawLine = function(ctx) {
// set style
- if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
- else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
- else {ctx.strokeStyle = this.color.color;}
- ctx.lineWidth = this._getLineWidth();
+ ctx.strokeStyle = this._getColor();
+ ctx.lineWidth = this._getLineWidth();
if (this.from != this.to) {
// draw line
- this._line(ctx);
+ var via = this._line(ctx);
// draw label
var point;
if (this.label) {
- if (this.smooth == true) {
- var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
- var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ if (this.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
point = {x:midpointX, y:midpointY};
}
else {
@@ -16825,6 +16883,154 @@ return /******/ (function(modules) { // webpackBootstrap
}
};
+ Edge.prototype._getViaCoordinates = function () {
+ var xVia = null;
+ var yVia = null;
+ var factor = this.smoothCurves.roundness;
+ var type = this.smoothCurves.type;
+ if (factor == 0) {
+ return {x:null,y:null};
+ }
+
+ var dx = Math.abs(this.from.x - this.to.x);
+ var dy = Math.abs(this.from.y - this.to.y);
+ if (type == 'discrete' || type == 'diagonalCross') {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ }
+ if (type == "discrete") {
+ xVia = dx < factor * dy ? this.from.x : xVia;
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ }
+ if (type == "discrete") {
+ yVia = dy < factor * dx ? this.from.y : yVia;
+ }
+ }
+ }
+ else if (type == "straightCross") {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ xVia = this.from.x;
+ yVia = this.to.y
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ xVia = this.to.x;
+ yVia = this.from.y;
+ }
+ }
+ else if (type == 'horizontal') {
+ xVia = this.to.x;
+ yVia = this.from.y;
+ }
+ else if (type == 'vertical') {
+ xVia = this.from.x;
+ yVia = this.to.y;
+ }
+ else { // continuous
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(1)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(2)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x > xVia ? this.to.x :xVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(3)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(4, this.from.x, this.to.x)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x > xVia ? this.to.x : xVia;
+ }
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(5)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(6)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(7)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(8)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
+ }
+ }
+ }
+ }
+
+
+ return {x:xVia, y:yVia};
+ }
+
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
@@ -16834,13 +17040,33 @@ return /******/ (function(modules) { // webpackBootstrap
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
- if (this.smooth == true) {
+ if (this.smoothCurves.enabled == true) {
+ if (this.smoothCurves.dynamic == false) {
+ var via = this._getViaCoordinates();
+ if (via.x == null) {
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
+ }
+ else {
+ // this.via.x = via.x;
+ // this.via.y = via.y;
+ ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return via;
+ }
+ }
+ else {
ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return this.via;
+ }
}
else {
ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
}
- ctx.stroke();
};
/**
@@ -16904,11 +17130,9 @@ return /******/ (function(modules) { // webpackBootstrap
ctx.lineWidth = this._getLineWidth();
+ var via = null;
// only firefox and chrome support this method, else we use the legacy one.
if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
- ctx.beginPath();
- ctx.moveTo(this.from.x, this.from.y);
-
// configure the dash pattern
var pattern = [0];
if (this.dash.length !== undefined && this.dash.gap !== undefined) {
@@ -16929,13 +17153,7 @@ return /******/ (function(modules) { // webpackBootstrap
}
// draw the line
- if (this.smooth == true) {
- ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
- }
- else {
- ctx.lineTo(this.to.x, this.to.y);
- }
- ctx.stroke();
+ via = this._line(ctx);
// restore the dash settings.
if (typeof ctx.setLineDash !== 'undefined') { //Chrome
@@ -16972,9 +17190,9 @@ return /******/ (function(modules) { // webpackBootstrap
// draw label
if (this.label) {
var point;
- if (this.smooth == true) {
- var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
- var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ if (this.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
point = {x:midpointX, y:midpointY};
}
else {
@@ -17031,14 +17249,14 @@ return /******/ (function(modules) { // webpackBootstrap
if (this.from != this.to) {
// draw line
- this._line(ctx);
+ var via = this._line(ctx);
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
var length = (10 + 5 * this.width) * this.arrowScaleFactor;
// draw an arrow halfway the line
- if (this.smooth == true) {
- var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
- var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ if (this.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
point = {x:midpointX, y:midpointY};
}
else {
@@ -17118,20 +17336,27 @@ return /******/ (function(modules) { // webpackBootstrap
var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+ var via;
+ if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true ) {
+ via = this.via;
+ }
+ else if (this.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
+ }
- if (this.smooth == true) {
- angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
- dx = (this.to.x - this.via.x);
- dy = (this.to.y - this.via.y);
+ if (this.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
}
var toBorderDist = this.to.distanceToBorder(ctx, angle);
var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
var xTo,yTo;
- if (this.smooth == true) {
- xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
+ if (this.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
}
else {
xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
@@ -17140,8 +17365,8 @@ return /******/ (function(modules) { // webpackBootstrap
ctx.beginPath();
ctx.moveTo(xFrom,yFrom);
- if (this.smooth == true) {
- ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
+ if (this.smoothCurves.enabled == true && via.x != null) {
+ ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
}
else {
ctx.lineTo(xTo, yTo);
@@ -17157,9 +17382,9 @@ return /******/ (function(modules) { // webpackBootstrap
// draw label
if (this.label) {
var point;
- if (this.smooth == true) {
- var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
- var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ if (this.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
point = {x:midpointX, y:midpointY};
}
else {
@@ -17229,13 +17454,23 @@ return /******/ (function(modules) { // webpackBootstrap
*/
Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
if (this.from != this.to) {
- if (this.smooth == true) {
+ if (this.smoothCurves.enabled == true) {
+ var xVia, yVia;
+ if (this.smoothCurves.enabled == true && this.smoothCurves.dynamic == true) {
+ xVia = this.via.x;
+ yVia = this.via.y;
+ }
+ else {
+ var via = this._getViaCoordinates();
+ xVia = via.x;
+ yVia = via.y;
+ }
var minDistance = 1e9;
var 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;
+ x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
+ y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
dx = Math.abs(x3-x);
dy = Math.abs(y3-y);
minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
@@ -17436,20 +17671,27 @@ return /******/ (function(modules) { // webpackBootstrap
var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+ var via;
+ if (this.smoothCurves.dynamic == true && this.smoothCurves.enabled == true) {
+ via = this.via;
+ }
+ else if (this.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
+ }
- if (this.smooth == true) {
- angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
- dx = (this.to.x - this.via.x);
- dy = (this.to.y - this.via.y);
+ if (this.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
}
var toBorderDist = this.to.distanceToBorder(ctx, angle);
var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
var xTo,yTo;
- if (this.smooth == true) {
- xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
+ if (this.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
}
else {
xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
@@ -17481,16 +17723,16 @@ return /******/ (function(modules) { // webpackBootstrap
* default constants for group colors
*/
Groups.DEFAULT = [
- {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
- {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
- {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
- {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
- {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
- {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
- {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
- {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
- {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
- {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
+ {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
+ {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
+ {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
+ {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
+ {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
+ {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
+ {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
+ {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
+ {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
+ {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
];
@@ -20266,7 +20508,7 @@ return /******/ (function(modules) { // webpackBootstrap
// Only load hammer.js when in a browser environment
// (loading hammer.js in a node.js environment gives errors)
if (typeof window !== 'undefined') {
- module.exports = window['Hammer'] || __webpack_require__(49);
+ module.exports = window['Hammer'] || __webpack_require__(50);
// TODO: throw an error when hammerjs is not available?
}
else {
@@ -20289,13 +20531,13 @@ return /******/ (function(modules) { // webpackBootstrap
/* 40 */
/***/ function(module, exports, __webpack_require__) {
- var PhysicsMixin = __webpack_require__(50);
- var ClusterMixin = __webpack_require__(43);
- var SectorsMixin = __webpack_require__(44);
- var SelectionMixin = __webpack_require__(45);
- var ManipulationMixin = __webpack_require__(46);
- var NavigationMixin = __webpack_require__(47);
- var HierarchicalLayoutMixin = __webpack_require__(48);
+ var PhysicsMixin = __webpack_require__(49);
+ var ClusterMixin = __webpack_require__(42);
+ var SectorsMixin = __webpack_require__(43);
+ var SelectionMixin = __webpack_require__(44);
+ var ManipulationMixin = __webpack_require__(45);
+ var NavigationMixin = __webpack_require__(46);
+ var HierarchicalLayoutMixin = __webpack_require__(47);
/**
* Load a mixin into the network object
@@ -20664,1946 +20906,1695 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
/**
- * Copyright 2012 Craig Campbell
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * Mousetrap is a simple keyboard shortcut library for Javascript with
- * no external dependencies
+ * Creation of the ClusterMixin var.
*
- * @version 1.1.2
- * @url craig.is/killing/mice
+ * This contains all the functions the Network object can use to employ clustering
*/
- /**
- * mapping of special keycodes to their corresponding keys
- *
- * everything in this dictionary cannot use keypress events
- * so it has to be here to map to the correct keycodes for
- * keyup/keydown events
- *
- * @type {Object}
- */
- var _MAP = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 18: 'alt',
- 20: 'capslock',
- 27: 'esc',
- 32: 'space',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 45: 'ins',
- 46: 'del',
- 91: 'meta',
- 93: 'meta',
- 224: 'meta'
- },
-
- /**
- * mapping for special characters so they can support
- *
- * this dictionary is only used incase you want to bind a
- * keyup or keydown event to one of these keys
- *
- * @type {Object}
- */
- _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- },
+ /**
+ * This is only called in the constructor of the network object
+ *
+ */
+ exports.startWithClustering = function() {
+ // cluster if the data set is big
+ this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
- /**
- * this is a mapping of keys that require shift on a US keypad
- * back to the non shift equivelents
- *
- * this is so you can use keyup events with these keys
- *
- * note that this will only work reliably on US keyboards
- *
- * @type {Object}
- */
- _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- },
+ // updates the lables after clustering
+ this.updateLabels();
- /**
- * this is a list of special strings you can use to map
- * to modifier keys when you specify your keyboard shortcuts
- *
- * @type {Object}
- */
- _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc'
- },
+ // this is called here because if clusterin is disabled, the start and stabilize are called in
+ // the setData function.
+ if (this.stabilize) {
+ this._stabilize();
+ }
+ this.start();
+ };
- /**
- * variable to store the flipped version of _MAP from above
- * needed to check if we should use keypress or not when no action
- * is specified
- *
- * @type {Object|undefined}
- */
- _REVERSE_MAP,
+ /**
+ * This function clusters until the initialMaxNodes has been reached
+ *
+ * @param {Number} maxNumberOfNodes
+ * @param {Boolean} reposition
+ */
+ exports.clusterToFit = function(maxNumberOfNodes, reposition) {
+ var numberOfNodes = this.nodeIndices.length;
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- _callbacks = {},
+ var maxLevels = 50;
+ var level = 0;
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- _direct_map = {},
+ // we first cluster the hubs, then we pull in the outliers, repeat
+ while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
+ if (level % 3 == 0) {
+ this.forceAggregateHubs(true);
+ this.normalizeClusterLevels();
+ }
+ else {
+ this.increaseClusterLevel(); // this also includes a cluster normalization
+ }
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- _sequence_levels = {},
+ numberOfNodes = this.nodeIndices.length;
+ level += 1;
+ }
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- _reset_timer,
+ // after the clustering we reposition the nodes to reduce the initial chaos
+ if (level > 0 && reposition == true) {
+ this.repositionNodes();
+ }
+ this._updateCalculationNodes();
+ };
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- _ignore_next_keyup = false,
+ /**
+ * This function can be called to open up a specific cluster. It is only called by
+ * It will unpack the cluster back one level.
+ *
+ * @param node | Node object: cluster to open.
+ */
+ exports.openCluster = function(node) {
+ var isMovingBeforeClustering = this.moving;
+ if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
+ !(this._sector() == "default" && this.nodeIndices.length == 1)) {
+ // this loads a new sector, loads the nodes and edges and nodeIndices of it.
+ this._addSector(node);
+ var level = 0;
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- _inside_sequence = false;
+ // we decluster until we reach a decent number of nodes
+ while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
+ this.decreaseClusterLevel();
+ level += 1;
+ }
- /**
- * loop through the f keys, f1 to f19 and add them to the map
- * programatically
- */
- for (var i = 1; i < 20; ++i) {
- _MAP[111 + i] = 'f' + i;
}
+ else {
+ this._expandClusterNode(node,false,true);
- /**
- * loop through to map numbers on the numeric keypad
- */
- for (i = 0; i <= 9; ++i) {
- _MAP[i + 96] = i;
+ // update the index list, dynamic edges and labels
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ this._updateCalculationNodes();
+ this.updateLabels();
}
- /**
- * cross browser add event method
- *
- * @param {Element|HTMLDocument} object
- * @param {string} type
- * @param {Function} callback
- * @returns void
- */
- function _addEvent(object, type, callback) {
- if (object.addEventListener) {
- return object.addEventListener(type, callback, false);
- }
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ };
- object.attachEvent('on' + type, callback);
+
+ /**
+ * This calls the updateClustes with default arguments
+ */
+ exports.updateClustersDefault = function() {
+ if (this.constants.clustering.enabled == true) {
+ this.updateClusters(0,false,false);
}
+ };
- /**
- * takes the event and returns the key character
- *
- * @param {Event} e
- * @return {string}
- */
- function _characterFromEvent(e) {
- // for keypress events we should return the character as is
- if (e.type == 'keypress') {
- return String.fromCharCode(e.which);
- }
+ /**
+ * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
+ * be clustered with their connected node. This can be repeated as many times as needed.
+ * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
+ */
+ exports.increaseClusterLevel = function() {
+ this.updateClusters(-1,false,true);
+ };
- // for non keypress events the special maps are needed
- if (_MAP[e.which]) {
- return _MAP[e.which];
- }
- if (_KEYCODE_MAP[e.which]) {
- return _KEYCODE_MAP[e.which];
- }
+ /**
+ * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
+ * be unpacked if they are a cluster. This can be repeated as many times as needed.
+ * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
+ */
+ exports.decreaseClusterLevel = function() {
+ this.updateClusters(1,false,true);
+ };
- // if it is not in the special map
- return String.fromCharCode(e.which).toLowerCase();
- }
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @return {boolean}
- */
- function _stop(e) {
- var element = e.target || e.srcElement,
- tag_name = element.tagName;
+ /**
+ * This is the main clustering function. It clusters and declusters on zoom or forced
+ * This function clusters on zoom, it can be called with a predefined zoom direction
+ * If out, check if we can form clusters, if in, check if we can open clusters.
+ * This function is only called from _zoom()
+ *
+ * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
+ * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
+ * @param {Boolean} force | enabled or disable forcing
+ * @param {Boolean} doNotStart | if true do not call start
+ *
+ */
+ exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
+ var isMovingBeforeClustering = this.moving;
+ var amountOfNodes = this.nodeIndices.length;
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
- }
+ // on zoom out collapse the sector if the scale is at the level the sector was made
+ if (this.previousScale > this.scale && zoomDirection == 0) {
+ this._collapseSector();
+ }
- // stop for input, select, and textarea
- return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ // check if we zoom in or out
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
+ // forming clusters when forced pulls outliers in. When not forced, the edge length of the
+ // outer nodes determines if it is being clustered
+ this._formClusters(force);
+ }
+ else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
+ if (force == true) {
+ // _openClusters checks for each node if the formationScale of the cluster is smaller than
+ // the current scale and if so, declusters. When forced, all clusters are reduced by one step
+ this._openClusters(recursive,force);
+ }
+ else {
+ // if a cluster takes up a set percentage of the active window
+ this._openClustersBySize();
+ }
}
+ this._updateNodeIndexList();
- /**
- * checks if two arrays are equal
- *
- * @param {Array} modifiers1
- * @param {Array} modifiers2
- * @returns {boolean}
- */
- function _modifiersMatch(modifiers1, modifiers2) {
- return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
+ if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
+ this._aggregateHubs(force);
+ this._updateNodeIndexList();
}
- /**
- * resets all sequence counters except for the ones passed in
- *
- * @param {Object} do_not_reset
- * @returns void
- */
- function _resetSequences(do_not_reset) {
- do_not_reset = do_not_reset || {};
+ // we now reduce chains.
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
+ this.handleChains();
+ this._updateNodeIndexList();
+ }
- var active_sequences = false,
- key;
+ this.previousScale = this.scale;
- for (key in _sequence_levels) {
- if (do_not_reset[key]) {
- active_sequences = true;
- continue;
- }
- _sequence_levels[key] = 0;
- }
+ // rest of the update the index list, dynamic edges and labels
+ this._updateDynamicEdges();
+ this.updateLabels();
- if (!active_sequences) {
- _inside_sequence = false;
- }
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
+ this.clusterSession += 1;
+ // if clusters have been made, we normalize the cluster level
+ this.normalizeClusterLevels();
}
- /**
- * finds all callbacks that match based on the keycode, modifiers,
- * and action
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {string} action
- * @param {boolean=} remove - should we remove any matches
- * @param {string=} combination
- * @returns {Array}
- */
- function _getMatches(character, modifiers, action, remove, combination) {
- var i,
- callback,
- matches = [];
+ if (doNotStart == false || doNotStart === undefined) {
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ }
- // if there are no events related to this keycode
- if (!_callbacks[character]) {
- return [];
- }
+ this._updateCalculationNodes();
+ };
- // if a modifier key is coming up on its own we should allow it
- if (action == 'keyup' && _isModifier(character)) {
- modifiers = [character];
- }
+ /**
+ * This function handles the chains. It is called on every updateClusters().
+ */
+ exports.handleChains = function() {
+ // after clustering we check how many chains there are
+ var chainPercentage = this._getChainFraction();
+ if (chainPercentage > this.constants.clustering.chainThreshold) {
+ this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
- // loop through all callbacks for the key that was pressed
- // and see if any of them match
- for (i = 0; i < _callbacks[character].length; ++i) {
- callback = _callbacks[character][i];
+ }
+ };
- // if this is a sequence but it is not at the right level
- // then move onto the next match
- if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
- continue;
- }
+ /**
+ * this functions starts clustering by hubs
+ * The minimum hub threshold is set globally
+ *
+ * @private
+ */
+ exports._aggregateHubs = function(force) {
+ this._getHubSize();
+ this._formClustersByHub(force,false);
+ };
- // if the action we are looking for doesn't match the action we got
- // then we should keep going
- if (action != callback.action) {
- continue;
- }
- // if this is a keypress event that means that we need to only
- // look at the character, otherwise check the modifiers as
- // well
- if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
+ /**
+ * This function is fired by keypress. It forces hubs to form.
+ *
+ */
+ exports.forceAggregateHubs = function(doNotStart) {
+ var isMovingBeforeClustering = this.moving;
+ var amountOfNodes = this.nodeIndices.length;
- // remove is used so if you change your mind and call bind a
- // second time with a new function the first one is overwritten
- if (remove && callback.combo == combination) {
- _callbacks[character].splice(i, 1);
- }
+ this._aggregateHubs(true);
- matches.push(callback);
- }
- }
+ // update the index list, dynamic edges and labels
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ this.updateLabels();
- return matches;
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length != amountOfNodes) {
+ this.clusterSession += 1;
}
- /**
- * takes a key event and figures out what the modifiers are
- *
- * @param {Event} e
- * @returns {Array}
- */
- function _eventModifiers(e) {
- var modifiers = [];
+ if (doNotStart == false || doNotStart === undefined) {
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ }
+ };
- if (e.shiftKey) {
- modifiers.push('shift');
+ /**
+ * If a cluster takes up more than a set percentage of the screen, open the cluster
+ *
+ * @private
+ */
+ exports._openClustersBySize = function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.inView() == true) {
+ if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
+ (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
+ this.openCluster(node);
+ }
}
+ }
+ }
+ };
- if (e.altKey) {
- modifiers.push('alt');
- }
- if (e.ctrlKey) {
- modifiers.push('ctrl');
- }
+ /**
+ * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
+ * has to be opened based on the current zoom level.
+ *
+ * @private
+ */
+ exports._openClusters = function(recursive,force) {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ this._expandClusterNode(node,recursive,force);
+ this._updateCalculationNodes();
+ }
+ };
- if (e.metaKey) {
- modifiers.push('meta');
- }
+ /**
+ * This function checks if a node has to be opened. This is done by checking the zoom level.
+ * If the node contains child nodes, this function is recursively called on the child nodes as well.
+ * This recursive behaviour is optional and can be set by the recursive argument.
+ *
+ * @param {Node} parentNode | to check for cluster and expand
+ * @param {Boolean} recursive | enabled or disable recursive calling
+ * @param {Boolean} force | enabled or disable forcing
+ * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
+ * @private
+ */
+ exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
+ // first check if node is a cluster
+ if (parentNode.clusterSize > 1) {
+ // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
+ if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
+ openAll = true;
+ }
+ recursive = openAll ? true : recursive;
- return modifiers;
- }
+ // if the last child has been added on a smaller scale than current scale decluster
+ if (parentNode.formationScale < this.scale || force == true) {
+ // we will check if any of the contained child nodes should be removed from the cluster
+ for (var containedNodeId in parentNode.containedNodes) {
+ if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
+ var childNode = parentNode.containedNodes[containedNodeId];
- /**
- * actually calls the callback function
- *
- * if your callback function returns false this will use the jquery
- * convention - prevent default and stop propogation on the event
- *
- * @param {Function} callback
- * @param {Event} e
- * @returns void
- */
- function _fireCallback(callback, e) {
- if (callback(e) === false) {
- if (e.preventDefault) {
- e.preventDefault();
+ // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
+ // the largest cluster is the one that comes from outside
+ if (force == true) {
+ if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
+ || openAll) {
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
+ }
}
-
- if (e.stopPropagation) {
- e.stopPropagation();
+ else {
+ if (this._nodeInActiveArea(parentNode)) {
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
+ }
}
-
- e.returnValue = false;
- e.cancelBubble = true;
+ }
}
+ }
}
+ };
- /**
- * handles a character key event
- *
- * @param {string} character
- * @param {Event} e
- * @returns void
- */
- function _handleCharacter(character, e) {
+ /**
+ * ONLY CALLED FROM _expandClusterNode
+ *
+ * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
+ * the child node from the parent contained_node object and put it back into the global nodes object.
+ * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
+ *
+ * @param {Node} parentNode | the parent node
+ * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
+ * @param {Boolean} recursive | This will also check if the child needs to be expanded.
+ * With force and recursive both true, the entire cluster is unpacked
+ * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
+ * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
+ * @private
+ */
+ exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
+ var childNode = parentNode.containedNodes[containedNodeId];
- // if this event should not happen stop here
- if (_stop(e)) {
- return;
- }
+ // if child node has been added on smaller scale than current, kick out
+ if (childNode.formationScale < this.scale || force == true) {
+ // unselect all selected items
+ this._unselectAll();
- var callbacks = _getMatches(character, _eventModifiers(e), e.type),
- i,
- do_not_reset = {},
- processed_sequence_callback = false;
+ // put the child node back in the global nodes object
+ this.nodes[containedNodeId] = childNode;
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
+ // release the contained edges from this childNode back into the global edges
+ this._releaseContainedEdges(parentNode,childNode);
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
- processed_sequence_callback = true;
+ // reconnect rerouted edges to the childNode
+ this._connectEdgeBackToChild(parentNode,childNode);
- // keep a list of which sequences were matches for later
- do_not_reset[callbacks[i].seq] = 1;
- _fireCallback(callbacks[i].callback, e);
- continue;
- }
+ // validate all edges in dynamicEdges
+ this._validateEdges(parentNode);
- // if there were no sequence matches but we are still here
- // that means this is a regular match so we should fire that
- if (!processed_sequence_callback && !_inside_sequence) {
- _fireCallback(callbacks[i].callback, e);
- }
- }
+ // undo the changes from the clustering operation on the parent node
+ parentNode.mass -= childNode.mass;
+ parentNode.clusterSize -= childNode.clusterSize;
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
+ parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
- // if you are inside of a sequence and the key you are pressing
- // is not a modifier key then we should reset all sequences
- // that were not matched by this key event
- if (e.type == _inside_sequence && !_isModifier(character)) {
- _resetSequences(do_not_reset);
+ // place the child node near the parent, not at the exact same location to avoid chaos in the system
+ childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
+ childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
+
+ // remove node from the list
+ delete parentNode.containedNodes[containedNodeId];
+
+ // check if there are other childs with this clusterSession in the parent.
+ var othersPresent = false;
+ for (var childNodeId in parentNode.containedNodes) {
+ if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
+ if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
+ othersPresent = true;
+ break;
+ }
}
- }
+ }
+ // if there are no others, remove the cluster session from the list
+ if (othersPresent == false) {
+ parentNode.clusterSessions.pop();
+ }
- /**
- * handles a keydown event
- *
- * @param {Event} e
- * @returns void
- */
- function _handleKey(e) {
+ this._repositionBezierNodes(childNode);
+ // this._repositionBezierNodes(parentNode);
- // normalize e.which for key events
- // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
- e.which = typeof e.which == "number" ? e.which : e.keyCode;
+ // remove the clusterSession from the child node
+ childNode.clusterSession = 0;
- var character = _characterFromEvent(e);
+ // recalculate the size of the node on the next time the node is rendered
+ parentNode.clearSizeCache();
- // no character found then stop
- if (!character) {
- return;
- }
+ // restart the simulation to reorganise all nodes
+ this.moving = true;
+ }
- if (e.type == 'keyup' && _ignore_next_keyup == character) {
- _ignore_next_keyup = false;
- return;
- }
+ // check if a further expansion step is possible if recursivity is enabled
+ if (recursive == true) {
+ this._expandClusterNode(childNode,recursive,force,openAll);
+ }
+ };
- _handleCharacter(character, e);
+
+ /**
+ * position the bezier nodes at the center of the edges
+ *
+ * @param node
+ * @private
+ */
+ exports._repositionBezierNodes = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ node.dynamicEdges[i].positionBezierNode();
}
+ };
- /**
- * determines if the keycode specified is a modifier key or not
- *
- * @param {string} key
- * @returns {boolean}
- */
- function _isModifier(key) {
- return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+
+ /**
+ * This function checks if any nodes at the end of their trees have edges below a threshold length
+ * This function is called only from updateClusters()
+ * forceLevelCollapse ignores the length of the edge and collapses one level
+ * This means that a node with only one edge will be clustered with its connected node
+ *
+ * @private
+ * @param {Boolean} force
+ */
+ exports._formClusters = function(force) {
+ if (force == false) {
+ this._formClustersByZoom();
+ }
+ else {
+ this._forceClustersByZoom();
}
+ };
- /**
- * called to set a 1 second timeout on the specified sequence
- *
- * this is so after each key press in the sequence you have 1 second
- * to press the next key before you have to start over
- *
- * @returns void
- */
- function _resetSequenceTimer() {
- clearTimeout(_reset_timer);
- _reset_timer = setTimeout(_resetSequences, 1000);
+
+ /**
+ * This function handles the clustering by zooming out, this is based on a minimum edge distance
+ *
+ * @private
+ */
+ exports._formClustersByZoom = function() {
+ var dx,dy,length,
+ minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
+
+ // check if any edges are shorter than minLength and start the clustering
+ // the clustering favours the node with the larger mass
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ var edge = this.edges[edgeId];
+ if (edge.connected) {
+ if (edge.toId != edge.fromId) {
+ dx = (edge.to.x - edge.from.x);
+ dy = (edge.to.y - edge.from.y);
+ length = Math.sqrt(dx * dx + dy * dy);
+
+
+ if (length < minLength) {
+ // first check which node is larger
+ var parentNode = edge.from;
+ var childNode = edge.to;
+ if (edge.to.mass > edge.from.mass) {
+ parentNode = edge.to;
+ childNode = edge.from;
+ }
+
+ if (childNode.dynamicEdgesLength == 1) {
+ this._addToCluster(parentNode,childNode,false);
+ }
+ else if (parentNode.dynamicEdgesLength == 1) {
+ this._addToCluster(childNode,parentNode,false);
+ }
+ }
+ }
+ }
+ }
}
+ };
- /**
- * reverses the map lookup so that we can look for specific keys
- * to see what can and can't use keypress
- *
- * @return {Object}
- */
- function _getReverseMap() {
- if (!_REVERSE_MAP) {
- _REVERSE_MAP = {};
- for (var key in _MAP) {
+ /**
+ * This function forces the network to cluster all nodes with only one connecting edge to their
+ * connected node.
+ *
+ * @private
+ */
+ exports._forceClustersByZoom = function() {
+ for (var nodeId in this.nodes) {
+ // another node could have absorbed this child.
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var childNode = this.nodes[nodeId];
- // pull out the numeric keypad from here cause keypress should
- // be able to detect the keys from the character
- if (key > 95 && key < 112) {
- continue;
- }
+ // the edges can be swallowed by another decrease
+ if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
+ var edge = childNode.dynamicEdges[0];
+ var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
- if (_MAP.hasOwnProperty(key)) {
- _REVERSE_MAP[_MAP[key]] = key;
- }
+ // group to the largest node
+ if (childNode.id != parentNode.id) {
+ if (parentNode.mass > childNode.mass) {
+ this._addToCluster(parentNode,childNode,true);
}
+ else {
+ this._addToCluster(childNode,parentNode,true);
+ }
+ }
}
- return _REVERSE_MAP;
+ }
}
+ };
- /**
- * picks the best action based on the key combination
- *
- * @param {string} key - character for key
- * @param {Array} modifiers
- * @param {string=} action passed in
- */
- function _pickBestAction(key, modifiers, action) {
- // if no action was picked in we should try to pick the one
- // that we think would work best for this key
- if (!action) {
- action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ /**
+ * To keep the nodes of roughly equal size we normalize the cluster levels.
+ * This function clusters a node to its smallest connected neighbour.
+ *
+ * @param node
+ * @private
+ */
+ exports._clusterToSmallestNeighbour = function(node) {
+ var smallestNeighbour = -1;
+ var smallestNeighbourNode = null;
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ if (node.dynamicEdges[i] !== undefined) {
+ var neighbour = null;
+ if (node.dynamicEdges[i].fromId != node.id) {
+ neighbour = node.dynamicEdges[i].from;
+ }
+ else if (node.dynamicEdges[i].toId != node.id) {
+ neighbour = node.dynamicEdges[i].to;
}
- // modifier keys don't work as expected with keypress,
- // switch to keydown
- if (action == 'keypress' && modifiers.length) {
- action = 'keydown';
+
+ if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
+ smallestNeighbour = neighbour.clusterSessions.length;
+ smallestNeighbourNode = neighbour;
}
+ }
+ }
- return action;
+ if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
+ this._addToCluster(neighbour, node, true);
}
+ };
- /**
- * binds a key sequence to an event
- *
- * @param {string} combo - combo specified in bind call
- * @param {Array} keys
- * @param {Function} callback
- * @param {string=} action
- * @returns void
- */
- function _bindSequence(combo, keys, callback, action) {
- // start off by adding a sequence level record for this combination
- // and setting the level to 0
- _sequence_levels[combo] = 0;
+ /**
+ * This function forms clusters from hubs, it loops over all nodes
+ *
+ * @param {Boolean} force | Disregard zoom level
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
+ * @private
+ */
+ exports._formClustersByHub = function(force, onlyEqual) {
+ // we loop over all nodes in the list
+ for (var nodeId in this.nodes) {
+ // we check if it is still available since it can be used by the clustering in this loop
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
+ }
+ }
+ };
- // if there is no action pick the best one for the first key
- // in the sequence
- if (!action) {
- action = _pickBestAction(keys[0], []);
- }
+ /**
+ * This function forms a cluster from a specific preselected hub node
+ *
+ * @param {Node} hubNode | the node we will cluster as a hub
+ * @param {Boolean} force | Disregard zoom level
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
+ * @param {Number} [absorptionSizeOffset] |
+ * @private
+ */
+ exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
+ if (absorptionSizeOffset === undefined) {
+ absorptionSizeOffset = 0;
+ }
+ // we decide if the node is a hub
+ if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
+ (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
+ // initialize variables
+ var dx,dy,length;
+ var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
+ var allowCluster = false;
- /**
- * callback to increase the sequence level for this sequence and reset
- * all other sequences that were active
- *
- * @param {Event} e
- * @returns void
- */
- var _increaseSequence = function(e) {
- _inside_sequence = action;
- ++_sequence_levels[combo];
- _resetSequenceTimer();
- },
+ // we create a list of edges because the dynamicEdges change over the course of this loop
+ var edgesIdarray = [];
+ var amountOfInitialEdges = hubNode.dynamicEdges.length;
+ for (var j = 0; j < amountOfInitialEdges; j++) {
+ edgesIdarray.push(hubNode.dynamicEdges[j].id);
+ }
- /**
- * wraps the specified callback inside of another function in order
- * to reset all sequence counters as soon as this sequence is done
- *
- * @param {Event} e
- * @returns void
- */
- _callbackAndReset = function(e) {
- _fireCallback(callback, e);
+ // if the hub clustering is not forces, we check if one of the edges connected
+ // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
+ if (force == false) {
+ allowCluster = false;
+ for (j = 0; j < amountOfInitialEdges; j++) {
+ var edge = this.edges[edgesIdarray[j]];
+ if (edge !== undefined) {
+ if (edge.connected) {
+ if (edge.toId != edge.fromId) {
+ dx = (edge.to.x - edge.from.x);
+ dy = (edge.to.y - edge.from.y);
+ length = Math.sqrt(dx * dx + dy * dy);
- // we should ignore the next key up if the action is key down
- // or keypress. this is so if you finish a sequence and
- // release the key the final key will not trigger a keyup
- if (action !== 'keyup') {
- _ignore_next_keyup = _characterFromEvent(e);
+ if (length < minLength) {
+ allowCluster = true;
+ break;
}
+ }
+ }
+ }
+ }
+ }
- // weird race condition if a sequence ends with the key
- // another sequence begins with
- setTimeout(_resetSequences, 10);
- },
- i;
-
- // loop through keys one at a time and bind the appropriate callback
- // function. for any key leading up to the final one it should
- // increase the sequence. after the final, it should reset all sequences
- for (i = 0; i < keys.length; ++i) {
- _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
+ // start the clustering if allowed
+ if ((!force && allowCluster) || force) {
+ // we loop over all edges INITIALLY connected to this hub
+ for (j = 0; j < amountOfInitialEdges; j++) {
+ edge = this.edges[edgesIdarray[j]];
+ // the edge can be clustered by this function in a previous loop
+ if (edge !== undefined) {
+ var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
+ // we do not want hubs to merge with other hubs nor do we want to cluster itself.
+ if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
+ (childNode.id != hubNode.id)) {
+ this._addToCluster(hubNode,childNode,force);
+ }
+ }
}
+ }
}
+ };
- /**
- * binds a single keyboard combination
- *
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequence_name - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
- */
- function _bindSingle(combination, callback, action, sequence_name, level) {
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
- var sequence = combination.split(' '),
- i,
- key,
- keys,
- modifiers = [];
+ /**
+ * This function adds the child node to the parent node, creating a cluster if it is not already.
+ *
+ * @param {Node} parentNode | this is the node that will house the child node
+ * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
+ * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
+ * @private
+ */
+ exports._addToCluster = function(parentNode, childNode, force) {
+ // join child node in the parent node
+ parentNode.containedNodes[childNode.id] = childNode;
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- return _bindSequence(combination, sequence, callback, action);
- }
+ // manage all the edges connected to the child and parent nodes
+ for (var i = 0; i < childNode.dynamicEdges.length; i++) {
+ var edge = childNode.dynamicEdges[i];
+ if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
+ this._addToContainedEdges(parentNode,childNode,edge);
+ }
+ else {
+ this._connectEdgeToCluster(parentNode,childNode,edge);
+ }
+ }
+ // a contained node has no dynamic edges.
+ childNode.dynamicEdges = [];
- // take the keys from this pattern and figure out what the actual
- // pattern is all about
- keys = combination === '+' ? ['+'] : combination.split('+');
+ // remove circular edges from clusters
+ this._containCircularEdgesFromNode(parentNode,childNode);
- for (i = 0; i < keys.length; ++i) {
- key = keys[i];
- // normalize key names
- if (_SPECIAL_ALIASES[key]) {
- key = _SPECIAL_ALIASES[key];
- }
+ // remove the childNode from the global nodes object
+ delete this.nodes[childNode.id];
- // if this is not a keypress event then we should
- // be smart about using shift keys
- // this will only work for US keyboards however
- if (action && action != 'keypress' && _SHIFT_MAP[key]) {
- key = _SHIFT_MAP[key];
- modifiers.push('shift');
- }
+ // update the properties of the child and parent
+ var massBefore = parentNode.mass;
+ childNode.clusterSession = this.clusterSession;
+ parentNode.mass += childNode.mass;
+ parentNode.clusterSize += childNode.clusterSize;
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
- // if this key is a modifier then add it to the list of modifiers
- if (_isModifier(key)) {
- modifiers.push(key);
- }
- }
+ // keep track of the clustersessions so we can open the cluster up as it has been formed.
+ if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
+ parentNode.clusterSessions.push(this.clusterSession);
+ }
- // depending on what the key combination is
- // we will try to pick the best event for it
- action = _pickBestAction(key, modifiers, action);
+ // forced clusters only open from screen size and double tap
+ if (force == true) {
+ // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
+ parentNode.formationScale = 0;
+ }
+ else {
+ parentNode.formationScale = this.scale; // The latest child has been added on this scale
+ }
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- if (!_callbacks[key]) {
- _callbacks[key] = [];
- }
+ // recalculate the size of the node on the next time the node is rendered
+ parentNode.clearSizeCache();
- // remove an existing match if there is one
- _getMatches(key, modifiers, action, !sequence_name, combination);
+ // set the pop-out scale for the childnode
+ parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
- // add this call back to the array
- // if it is a sequence put it at the beginning
- // if not put it at the end
- //
- // this is important because the way these are processed expects
- // the sequence ones to come first
- _callbacks[key][sequence_name ? 'unshift' : 'push']({
- callback: callback,
- modifiers: modifiers,
- action: action,
- seq: sequence_name,
- level: level,
- combo: combination
- });
- }
-
- /**
- * binds multiple combinations to the same callback
- *
- * @param {Array} combinations
- * @param {Function} callback
- * @param {string|undefined} action
- * @returns void
- */
- function _bindMultiple(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
- }
- }
-
- // start!
- _addEvent(document, 'keypress', _handleKey);
- _addEvent(document, 'keydown', _handleKey);
- _addEvent(document, 'keyup', _handleKey);
-
- var mousetrap = {
-
- /**
- * binds an event to mousetrap
- *
- * can be a single key, a combination of keys separated with +,
- * a comma separated list of keys, an array of keys, or
- * a sequence of keys separated by spaces
- *
- * be sure to list the modifier keys first to make sure that the
- * correct key ends up getting bound (the last key in the pattern)
- *
- * @param {string|Array} keys
- * @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
- * @returns void
- */
- bind: function(keys, callback, action) {
- _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
- _direct_map[keys + ':' + action] = callback;
- return this;
- },
-
- /**
- * unbinds an event to mousetrap
- *
- * the unbinding sets the callback function of the specified key combo
- * to an empty function and deletes the corresponding key in the
- * _direct_map dict.
- *
- * the keycombo+action has to be exactly the same as
- * it was defined in the bind method
- *
- * TODO: actually remove this from the _callbacks dictionary instead
- * of binding an empty function
- *
- * @param {string|Array} keys
- * @param {string} action
- * @returns void
- */
- unbind: function(keys, action) {
- if (_direct_map[keys + ':' + action]) {
- delete _direct_map[keys + ':' + action];
- this.bind(keys, function() {}, action);
- }
- return this;
- },
-
- /**
- * triggers an event that has already been bound
- *
- * @param {string} keys
- * @param {string=} action
- * @returns void
- */
- trigger: function(keys, action) {
- _direct_map[keys + ':' + action]();
- return this;
- },
-
- /**
- * resets the library back to its initial state. this is useful
- * if you want to clear out the current keyboard shortcuts and bind
- * new ones - for example if you switch to another page
- *
- * @returns void
- */
- reset: function() {
- _callbacks = {};
- _direct_map = {};
- return this;
- }
- };
-
- module.exports = mousetrap;
+ // nullify the movement velocity of the child, this is to avoid hectic behaviour
+ childNode.clearVelocity();
+ // the mass has altered, preservation of energy dictates the velocity to be updated
+ parentNode.updateVelocity(massBefore);
+ // restart the simulation to reorganise all nodes
+ this.moving = true;
+ };
-/***/ },
-/* 43 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * Creation of the ClusterMixin var.
- *
- * This contains all the functions the Network object can use to employ clustering
+ * This function will apply the changes made to the remainingEdges during the formation of the clusters.
+ * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
+ * It has to be called if a level is collapsed. It is called by _formClusters().
+ * @private
*/
+ exports._updateDynamicEdges = function() {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ node.dynamicEdgesLength = node.dynamicEdges.length;
- /**
- * This is only called in the constructor of the network object
- *
- */
- exports.startWithClustering = function() {
- // cluster if the data set is big
- this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
-
- // updates the lables after clustering
- this.updateLabels();
-
- // this is called here because if clusterin is disabled, the start and stabilize are called in
- // the setData function.
- if (this.stabilize) {
- this._stabilize();
- }
- this.start();
+ // this corrects for multiple edges pointing at the same other node
+ var correction = 0;
+ if (node.dynamicEdgesLength > 1) {
+ for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
+ var edgeToId = node.dynamicEdges[j].toId;
+ var edgeFromId = node.dynamicEdges[j].fromId;
+ for (var k = j+1; k < node.dynamicEdgesLength; k++) {
+ if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
+ (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
+ correction += 1;
+ }
+ }
+ }
+ }
+ node.dynamicEdgesLength -= correction;
+ }
};
+
/**
- * This function clusters until the initialMaxNodes has been reached
+ * This adds an edge from the childNode to the contained edges of the parent node
*
- * @param {Number} maxNumberOfNodes
- * @param {Boolean} reposition
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @param edge | Edge object
+ * @private
*/
- exports.clusterToFit = function(maxNumberOfNodes, reposition) {
- var numberOfNodes = this.nodeIndices.length;
+ exports._addToContainedEdges = function(parentNode, childNode, edge) {
+ // create an array object if it does not yet exist for this childNode
+ if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
+ parentNode.containedEdges[childNode.id] = []
+ }
+ // add this edge to the list
+ parentNode.containedEdges[childNode.id].push(edge);
- var maxLevels = 50;
- var level = 0;
+ // remove the edge from the global edges object
+ delete this.edges[edge.id];
- // we first cluster the hubs, then we pull in the outliers, repeat
- while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
- if (level % 3 == 0) {
- this.forceAggregateHubs(true);
- this.normalizeClusterLevels();
- }
- else {
- this.increaseClusterLevel(); // this also includes a cluster normalization
+ // remove the edge from the parent object
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ if (parentNode.dynamicEdges[i].id == edge.id) {
+ parentNode.dynamicEdges.splice(i,1);
+ break;
}
-
- numberOfNodes = this.nodeIndices.length;
- level += 1;
- }
-
- // after the clustering we reposition the nodes to reduce the initial chaos
- if (level > 0 && reposition == true) {
- this.repositionNodes();
}
- this._updateCalculationNodes();
};
/**
- * This function can be called to open up a specific cluster. It is only called by
- * It will unpack the cluster back one level.
+ * This function connects an edge that was connected to a child node to the parent node.
+ * It keeps track of which nodes it has been connected to with the originalId array.
*
- * @param node | Node object: cluster to open.
+ * @param {Node} parentNode | Node object
+ * @param {Node} childNode | Node object
+ * @param {Edge} edge | Edge object
+ * @private
*/
- exports.openCluster = function(node) {
- var isMovingBeforeClustering = this.moving;
- if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
- !(this._sector() == "default" && this.nodeIndices.length == 1)) {
- // this loads a new sector, loads the nodes and edges and nodeIndices of it.
- this._addSector(node);
- var level = 0;
-
- // we decluster until we reach a decent number of nodes
- while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
- this.decreaseClusterLevel();
- level += 1;
- }
-
+ exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
+ // handle circular edges
+ if (edge.toId == edge.fromId) {
+ this._addToContainedEdges(parentNode, childNode, edge);
}
else {
- this._expandClusterNode(node,false,true);
+ if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
+ edge.originalToId.push(childNode.id);
+ edge.to = parentNode;
+ edge.toId = parentNode.id;
+ }
+ else { // edge connected to other node with the "from" side
- // update the index list, dynamic edges and labels
- this._updateNodeIndexList();
- this._updateDynamicEdges();
- this._updateCalculationNodes();
- this.updateLabels();
- }
+ edge.originalFromId.push(childNode.id);
+ edge.from = parentNode;
+ edge.fromId = parentNode.id;
+ }
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
+ this._addToReroutedEdges(parentNode,childNode,edge);
}
};
/**
- * This calls the updateClustes with default arguments
+ * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
+ * these edges inside of the cluster.
+ *
+ * @param parentNode
+ * @param childNode
+ * @private
*/
- exports.updateClustersDefault = function() {
- if (this.constants.clustering.enabled == true) {
- this.updateClusters(0,false,false);
+ exports._containCircularEdgesFromNode = function(parentNode, childNode) {
+ // manage all the edges connected to the child and parent nodes
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ var edge = parentNode.dynamicEdges[i];
+ // handle circular edges
+ if (edge.toId == edge.fromId) {
+ this._addToContainedEdges(parentNode, childNode, edge);
+ }
}
};
/**
- * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
- * be clustered with their connected node. This can be repeated as many times as needed.
- * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
- */
- exports.increaseClusterLevel = function() {
- this.updateClusters(-1,false,true);
- };
-
-
- /**
- * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
- * be unpacked if they are a cluster. This can be repeated as many times as needed.
- * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
- */
- exports.decreaseClusterLevel = function() {
- this.updateClusters(1,false,true);
- };
-
-
- /**
- * This is the main clustering function. It clusters and declusters on zoom or forced
- * This function clusters on zoom, it can be called with a predefined zoom direction
- * If out, check if we can form clusters, if in, check if we can open clusters.
- * This function is only called from _zoom()
- *
- * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
- * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
- * @param {Boolean} force | enabled or disable forcing
- * @param {Boolean} doNotStart | if true do not call start
+ * This adds an edge from the childNode to the rerouted edges of the parent node
*
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @param edge | Edge object
+ * @private
*/
- exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
- var isMovingBeforeClustering = this.moving;
- var amountOfNodes = this.nodeIndices.length;
-
- // on zoom out collapse the sector if the scale is at the level the sector was made
- if (this.previousScale > this.scale && zoomDirection == 0) {
- this._collapseSector();
- }
-
- // check if we zoom in or out
- if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
- // forming clusters when forced pulls outliers in. When not forced, the edge length of the
- // outer nodes determines if it is being clustered
- this._formClusters(force);
- }
- else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
- if (force == true) {
- // _openClusters checks for each node if the formationScale of the cluster is smaller than
- // the current scale and if so, declusters. When forced, all clusters are reduced by one step
- this._openClusters(recursive,force);
- }
- else {
- // if a cluster takes up a set percentage of the active window
- this._openClustersBySize();
- }
+ exports._addToReroutedEdges = function(parentNode, childNode, edge) {
+ // create an array object if it does not yet exist for this childNode
+ // we store the edge in the rerouted edges so we can restore it when the cluster pops open
+ if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
+ parentNode.reroutedEdges[childNode.id] = [];
}
- this._updateNodeIndexList();
+ parentNode.reroutedEdges[childNode.id].push(edge);
- // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
- if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
- this._aggregateHubs(force);
- this._updateNodeIndexList();
- }
+ // this edge becomes part of the dynamicEdges of the cluster node
+ parentNode.dynamicEdges.push(edge);
+ };
- // we now reduce chains.
- if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
- this.handleChains();
- this._updateNodeIndexList();
- }
- this.previousScale = this.scale;
- // rest of the update the index list, dynamic edges and labels
- this._updateDynamicEdges();
- this.updateLabels();
+ /**
+ * This function connects an edge that was connected to a cluster node back to the child node.
+ *
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @private
+ */
+ exports._connectEdgeBackToChild = function(parentNode, childNode) {
+ if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
+ for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
+ var edge = parentNode.reroutedEdges[childNode.id][i];
+ if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
+ edge.originalFromId.pop();
+ edge.fromId = childNode.id;
+ edge.from = childNode;
+ }
+ else {
+ edge.originalToId.pop();
+ edge.toId = childNode.id;
+ edge.to = childNode;
+ }
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
- this.clusterSession += 1;
- // if clusters have been made, we normalize the cluster level
- this.normalizeClusterLevels();
- }
+ // append this edge to the list of edges connecting to the childnode
+ childNode.dynamicEdges.push(edge);
- if (doNotStart == false || doNotStart === undefined) {
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
+ // remove the edge from the parent object
+ for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
+ if (parentNode.dynamicEdges[j].id == edge.id) {
+ parentNode.dynamicEdges.splice(j,1);
+ break;
+ }
+ }
}
+ // remove the entry from the rerouted edges
+ delete parentNode.reroutedEdges[childNode.id];
}
-
- this._updateCalculationNodes();
};
- /**
- * This function handles the chains. It is called on every updateClusters().
- */
- exports.handleChains = function() {
- // after clustering we check how many chains there are
- var chainPercentage = this._getChainFraction();
- if (chainPercentage > this.constants.clustering.chainThreshold) {
- this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
-
- }
- };
/**
- * this functions starts clustering by hubs
- * The minimum hub threshold is set globally
+ * When loops are clustered, an edge can be both in the rerouted array and the contained array.
+ * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
+ * parentNode
*
+ * @param parentNode | Node object
* @private
*/
- exports._aggregateHubs = function(force) {
- this._getHubSize();
- this._formClustersByHub(force,false);
+ exports._validateEdges = function(parentNode) {
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ var edge = parentNode.dynamicEdges[i];
+ if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
+ parentNode.dynamicEdges.splice(i,1);
+ }
+ }
};
/**
- * This function is fired by keypress. It forces hubs to form.
+ * This function released the contained edges back into the global domain and puts them back into the
+ * dynamic edges of both parent and child.
*
+ * @param {Node} parentNode |
+ * @param {Node} childNode |
+ * @private
*/
- exports.forceAggregateHubs = function(doNotStart) {
- var isMovingBeforeClustering = this.moving;
- var amountOfNodes = this.nodeIndices.length;
-
- this._aggregateHubs(true);
+ exports._releaseContainedEdges = function(parentNode, childNode) {
+ for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
+ var edge = parentNode.containedEdges[childNode.id][i];
- // update the index list, dynamic edges and labels
- this._updateNodeIndexList();
- this._updateDynamicEdges();
- this.updateLabels();
+ // put the edge back in the global edges object
+ this.edges[edge.id] = edge;
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length != amountOfNodes) {
- this.clusterSession += 1;
+ // put the edge back in the dynamic edges of the child and parent
+ childNode.dynamicEdges.push(edge);
+ parentNode.dynamicEdges.push(edge);
}
+ // remove the entry from the contained edges
+ delete parentNode.containedEdges[childNode.id];
- if (doNotStart == false || doNotStart === undefined) {
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
- }
- }
};
+
+
+
+ // ------------------- UTILITY FUNCTIONS ---------------------------- //
+
+
/**
- * If a cluster takes up more than a set percentage of the screen, open the cluster
- *
- * @private
+ * This updates the node labels for all nodes (for debugging purposes)
*/
- exports._openClustersBySize = function() {
- for (var nodeId in this.nodes) {
+ exports.updateLabels = function() {
+ var nodeId;
+ // update node labels
+ for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
- if (node.inView() == true) {
- if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
- (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
- this.openCluster(node);
+ if (node.clusterSize > 1) {
+ node.label = "[".concat(String(node.clusterSize),"]");
+ }
+ }
+ }
+
+ // update node labels
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.clusterSize == 1) {
+ if (node.originalLabel !== undefined) {
+ node.label = node.originalLabel;
+ }
+ else {
+ node.label = String(node.id);
}
}
}
}
+
+ // /* Debug Override */
+ // for (nodeId in this.nodes) {
+ // if (this.nodes.hasOwnProperty(nodeId)) {
+ // node = this.nodes[nodeId];
+ // node.label = String(node.level);
+ // }
+ // }
+
};
/**
- * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
- * has to be opened based on the current zoom level.
- *
- * @private
+ * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
+ * if the rest of the nodes are already a few cluster levels in.
+ * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
+ * clustered enough to the clusterToSmallestNeighbours function.
*/
- exports._openClusters = function(recursive,force) {
- for (var i = 0; i < this.nodeIndices.length; i++) {
- var node = this.nodes[this.nodeIndices[i]];
- this._expandClusterNode(node,recursive,force);
- this._updateCalculationNodes();
+ exports.normalizeClusterLevels = function() {
+ var maxLevel = 0;
+ var minLevel = 1e9;
+ var clusterLevel = 0;
+ var nodeId;
+
+ // we loop over all nodes in the list
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ clusterLevel = this.nodes[nodeId].clusterSessions.length;
+ if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
+ if (minLevel > clusterLevel) {minLevel = clusterLevel;}
+ }
}
- };
- /**
- * This function checks if a node has to be opened. This is done by checking the zoom level.
- * If the node contains child nodes, this function is recursively called on the child nodes as well.
- * This recursive behaviour is optional and can be set by the recursive argument.
+ if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
+ var amountOfNodes = this.nodeIndices.length;
+ var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
+ // we loop over all nodes in the list
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
+ this._clusterToSmallestNeighbour(this.nodes[nodeId]);
+ }
+ }
+ }
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length != amountOfNodes) {
+ this.clusterSession += 1;
+ }
+ }
+ };
+
+
+
+ /**
+ * This function determines if the cluster we want to decluster is in the active area
+ * this means around the zoom center
*
- * @param {Node} parentNode | to check for cluster and expand
- * @param {Boolean} recursive | enabled or disable recursive calling
- * @param {Boolean} force | enabled or disable forcing
- * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
+ * @param {Node} node
+ * @returns {boolean}
* @private
*/
- exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
- // first check if node is a cluster
- if (parentNode.clusterSize > 1) {
- // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
- if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
- openAll = true;
- }
- recursive = openAll ? true : recursive;
+ exports._nodeInActiveArea = function(node) {
+ return (
+ Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
+ &&
+ Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
+ )
+ };
- // if the last child has been added on a smaller scale than current scale decluster
- if (parentNode.formationScale < this.scale || force == true) {
- // we will check if any of the contained child nodes should be removed from the cluster
- for (var containedNodeId in parentNode.containedNodes) {
- if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
- var childNode = parentNode.containedNodes[containedNodeId];
- // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
- // the largest cluster is the one that comes from outside
- if (force == true) {
- if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
- || openAll) {
- this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
- }
- }
- else {
- if (this._nodeInActiveArea(parentNode)) {
- this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
- }
- }
- }
- }
+ /**
+ * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
+ * It puts large clusters away from the center and randomizes the order.
+ *
+ */
+ exports.repositionNodes = function() {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ if ((node.xFixed == false || node.yFixed == false)) {
+ var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
+ var angle = 2 * Math.PI * Math.random();
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
+ this._repositionBezierNodes(node);
}
}
};
+
/**
- * ONLY CALLED FROM _expandClusterNode
- *
- * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
- * the child node from the parent contained_node object and put it back into the global nodes object.
- * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
+ * We determine how many connections denote an important hub.
+ * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
*
- * @param {Node} parentNode | the parent node
- * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
- * @param {Boolean} recursive | This will also check if the child needs to be expanded.
- * With force and recursive both true, the entire cluster is unpacked
- * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
- * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private
*/
- exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
- var childNode = parentNode.containedNodes[containedNodeId];
+ exports._getHubSize = function() {
+ var average = 0;
+ var averageSquared = 0;
+ var hubCounter = 0;
+ var largestHub = 0;
- // if child node has been added on smaller scale than current, kick out
- if (childNode.formationScale < this.scale || force == true) {
- // unselect all selected items
- this._unselectAll();
+ for (var i = 0; i < this.nodeIndices.length; i++) {
- // put the child node back in the global nodes object
- this.nodes[containedNodeId] = childNode;
+ var node = this.nodes[this.nodeIndices[i]];
+ if (node.dynamicEdgesLength > largestHub) {
+ largestHub = node.dynamicEdgesLength;
+ }
+ average += node.dynamicEdgesLength;
+ averageSquared += Math.pow(node.dynamicEdgesLength,2);
+ hubCounter += 1;
+ }
+ average = average / hubCounter;
+ averageSquared = averageSquared / hubCounter;
- // release the contained edges from this childNode back into the global edges
- this._releaseContainedEdges(parentNode,childNode);
+ var variance = averageSquared - Math.pow(average,2);
- // reconnect rerouted edges to the childNode
- this._connectEdgeBackToChild(parentNode,childNode);
+ var standardDeviation = Math.sqrt(variance);
- // validate all edges in dynamicEdges
- this._validateEdges(parentNode);
+ this.hubThreshold = Math.floor(average + 2*standardDeviation);
- // undo the changes from the clustering operation on the parent node
- parentNode.mass -= childNode.mass;
- parentNode.clusterSize -= childNode.clusterSize;
- parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
- parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
+ // always have at least one to cluster
+ if (this.hubThreshold > largestHub) {
+ this.hubThreshold = largestHub;
+ }
- // place the child node near the parent, not at the exact same location to avoid chaos in the system
- childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
- childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
+ // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
+ // console.log("hubThreshold:",this.hubThreshold);
+ };
- // remove node from the list
- delete parentNode.containedNodes[containedNodeId];
- // check if there are other childs with this clusterSession in the parent.
- var othersPresent = false;
- for (var childNodeId in parentNode.containedNodes) {
- if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
- if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
- othersPresent = true;
- break;
+ /**
+ * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
+ * with this amount we can cluster specifically on these chains.
+ *
+ * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
+ * @private
+ */
+ exports._reduceAmountOfChains = function(fraction) {
+ this.hubThreshold = 2;
+ var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
+ if (reduceAmount > 0) {
+ this._formClusterFromHub(this.nodes[nodeId],true,true,1);
+ reduceAmount -= 1;
}
}
}
- // if there are no others, remove the cluster session from the list
- if (othersPresent == false) {
- parentNode.clusterSessions.pop();
- }
-
- this._repositionBezierNodes(childNode);
- // this._repositionBezierNodes(parentNode);
-
- // remove the clusterSession from the child node
- childNode.clusterSession = 0;
-
- // recalculate the size of the node on the next time the node is rendered
- parentNode.clearSizeCache();
-
- // restart the simulation to reorganise all nodes
- this.moving = true;
}
+ };
- // check if a further expansion step is possible if recursivity is enabled
- if (recursive == true) {
- this._expandClusterNode(childNode,recursive,force,openAll);
+ /**
+ * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
+ * with this amount we can cluster specifically on these chains.
+ *
+ * @private
+ */
+ exports._getChainFraction = function() {
+ var chains = 0;
+ var total = 0;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
+ chains += 1;
+ }
+ total += 1;
+ }
}
+ return chains/total;
};
+/***/ },
+/* 43 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+
/**
- * position the bezier nodes at the center of the edges
+ * Creation of the SectorMixin var.
+ *
+ * This contains all the functions the Network object can use to employ the sector system.
+ * The sector system is always used by Network, though the benefits only apply to the use of clustering.
+ * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
+ */
+
+ /**
+ * This function is only called by the setData function of the Network object.
+ * This loads the global references into the active sector. This initializes the sector.
*
- * @param node
* @private
*/
- exports._repositionBezierNodes = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- node.dynamicEdges[i].positionBezierNode();
- }
+ exports._putDataInSector = function() {
+ this.sectors["active"][this._sector()].nodes = this.nodes;
+ this.sectors["active"][this._sector()].edges = this.edges;
+ this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
};
/**
- * This function checks if any nodes at the end of their trees have edges below a threshold length
- * This function is called only from updateClusters()
- * forceLevelCollapse ignores the length of the edge and collapses one level
- * This means that a node with only one edge will be clustered with its connected node
+ * /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied (active) sector. If a type is defined, do the specific type
*
+ * @param {String} sectorId
+ * @param {String} [sectorType] | "active" or "frozen"
* @private
- * @param {Boolean} force
*/
- exports._formClusters = function(force) {
- if (force == false) {
- this._formClustersByZoom();
+ exports._switchToSector = function(sectorId, sectorType) {
+ if (sectorType === undefined || sectorType == "active") {
+ this._switchToActiveSector(sectorId);
}
else {
- this._forceClustersByZoom();
+ this._switchToFrozenSector(sectorId);
}
};
/**
- * This function handles the clustering by zooming out, this is based on a minimum edge distance
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied active sector.
*
+ * @param sectorId
* @private
*/
- exports._formClustersByZoom = function() {
- var dx,dy,length,
- minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
-
- // check if any edges are shorter than minLength and start the clustering
- // the clustering favours the node with the larger mass
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- var edge = this.edges[edgeId];
- if (edge.connected) {
- if (edge.toId != edge.fromId) {
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- length = Math.sqrt(dx * dx + dy * dy);
-
+ exports._switchToActiveSector = function(sectorId) {
+ this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
+ this.nodes = this.sectors["active"][sectorId]["nodes"];
+ this.edges = this.sectors["active"][sectorId]["edges"];
+ };
- if (length < minLength) {
- // first check which node is larger
- var parentNode = edge.from;
- var childNode = edge.to;
- if (edge.to.mass > edge.from.mass) {
- parentNode = edge.to;
- childNode = edge.from;
- }
- if (childNode.dynamicEdgesLength == 1) {
- this._addToCluster(parentNode,childNode,false);
- }
- else if (parentNode.dynamicEdgesLength == 1) {
- this._addToCluster(childNode,parentNode,false);
- }
- }
- }
- }
- }
- }
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied active sector.
+ *
+ * @private
+ */
+ exports._switchToSupportSector = function() {
+ this.nodeIndices = this.sectors["support"]["nodeIndices"];
+ this.nodes = this.sectors["support"]["nodes"];
+ this.edges = this.sectors["support"]["edges"];
};
+
/**
- * This function forces the network to cluster all nodes with only one connecting edge to their
- * connected node.
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied frozen sector.
*
+ * @param sectorId
* @private
*/
- exports._forceClustersByZoom = function() {
- for (var nodeId in this.nodes) {
- // another node could have absorbed this child.
- if (this.nodes.hasOwnProperty(nodeId)) {
- var childNode = this.nodes[nodeId];
+ exports._switchToFrozenSector = function(sectorId) {
+ this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
+ this.nodes = this.sectors["frozen"][sectorId]["nodes"];
+ this.edges = this.sectors["frozen"][sectorId]["edges"];
+ };
- // the edges can be swallowed by another decrease
- if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
- var edge = childNode.dynamicEdges[0];
- var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
- // group to the largest node
- if (childNode.id != parentNode.id) {
- if (parentNode.mass > childNode.mass) {
- this._addToCluster(parentNode,childNode,true);
- }
- else {
- this._addToCluster(childNode,parentNode,true);
- }
- }
- }
- }
- }
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the currently active sector.
+ *
+ * @private
+ */
+ exports._loadLatestSector = function() {
+ this._switchToSector(this._sector());
};
/**
- * To keep the nodes of roughly equal size we normalize the cluster levels.
- * This function clusters a node to its smallest connected neighbour.
+ * This function returns the currently active sector Id
*
- * @param node
+ * @returns {String}
* @private
*/
- exports._clusterToSmallestNeighbour = function(node) {
- var smallestNeighbour = -1;
- var smallestNeighbourNode = null;
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- if (node.dynamicEdges[i] !== undefined) {
- var neighbour = null;
- if (node.dynamicEdges[i].fromId != node.id) {
- neighbour = node.dynamicEdges[i].from;
- }
- else if (node.dynamicEdges[i].toId != node.id) {
- neighbour = node.dynamicEdges[i].to;
- }
+ exports._sector = function() {
+ return this.activeSector[this.activeSector.length-1];
+ };
- if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
- smallestNeighbour = neighbour.clusterSessions.length;
- smallestNeighbourNode = neighbour;
- }
- }
+ /**
+ * This function returns the previously active sector Id
+ *
+ * @returns {String}
+ * @private
+ */
+ exports._previousSector = function() {
+ if (this.activeSector.length > 1) {
+ return this.activeSector[this.activeSector.length-2];
}
-
- if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
- this._addToCluster(neighbour, node, true);
+ else {
+ throw new TypeError('there are not enough sectors in the this.activeSector array.');
}
};
/**
- * This function forms clusters from hubs, it loops over all nodes
+ * We add the active sector at the end of the this.activeSector array
+ * This ensures it is the currently active sector returned by _sector() and it reaches the top
+ * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
*
- * @param {Boolean} force | Disregard zoom level
- * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
+ * @param newId
* @private
*/
- exports._formClustersByHub = function(force, onlyEqual) {
- // we loop over all nodes in the list
- for (var nodeId in this.nodes) {
- // we check if it is still available since it can be used by the clustering in this loop
- if (this.nodes.hasOwnProperty(nodeId)) {
- this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
- }
- }
+ exports._setActiveSector = function(newId) {
+ this.activeSector.push(newId);
};
+
/**
- * This function forms a cluster from a specific preselected hub node
+ * We remove the currently active sector id from the active sector stack. This happens when
+ * we reactivate the previously active sector
*
- * @param {Node} hubNode | the node we will cluster as a hub
- * @param {Boolean} force | Disregard zoom level
- * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
- * @param {Number} [absorptionSizeOffset] |
* @private
*/
- exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
- if (absorptionSizeOffset === undefined) {
- absorptionSizeOffset = 0;
- }
- // we decide if the node is a hub
- if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
- (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
- // initialize variables
- var dx,dy,length;
- var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
- var allowCluster = false;
-
- // we create a list of edges because the dynamicEdges change over the course of this loop
- var edgesIdarray = [];
- var amountOfInitialEdges = hubNode.dynamicEdges.length;
- for (var j = 0; j < amountOfInitialEdges; j++) {
- edgesIdarray.push(hubNode.dynamicEdges[j].id);
- }
+ exports._forgetLastSector = function() {
+ this.activeSector.pop();
+ };
- // if the hub clustering is not forces, we check if one of the edges connected
- // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
- if (force == false) {
- allowCluster = false;
- for (j = 0; j < amountOfInitialEdges; j++) {
- var edge = this.edges[edgesIdarray[j]];
- if (edge !== undefined) {
- if (edge.connected) {
- if (edge.toId != edge.fromId) {
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- length = Math.sqrt(dx * dx + dy * dy);
- if (length < minLength) {
- allowCluster = true;
- break;
- }
- }
- }
- }
- }
- }
+ /**
+ * This function creates a new active sector with the supplied newId. This newId
+ * is the expanding node id.
+ *
+ * @param {String} newId | Id of the new active sector
+ * @private
+ */
+ exports._createNewSector = function(newId) {
+ // create the new sector
+ this.sectors["active"][newId] = {"nodes":{},
+ "edges":{},
+ "nodeIndices":[],
+ "formationScale": this.scale,
+ "drawingNode": undefined};
- // start the clustering if allowed
- if ((!force && allowCluster) || force) {
- // we loop over all edges INITIALLY connected to this hub
- for (j = 0; j < amountOfInitialEdges; j++) {
- edge = this.edges[edgesIdarray[j]];
- // the edge can be clustered by this function in a previous loop
- if (edge !== undefined) {
- var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
- // we do not want hubs to merge with other hubs nor do we want to cluster itself.
- if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
- (childNode.id != hubNode.id)) {
- this._addToCluster(hubNode,childNode,force);
- }
+ // create the new sector render node. This gives visual feedback that you are in a new sector.
+ this.sectors["active"][newId]['drawingNode'] = new Node(
+ {id:newId,
+ color: {
+ background: "#eaefef",
+ border: "495c5e"
}
- }
- }
- }
+ },{},{},this.constants);
+ this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
};
+ /**
+ * This function removes the currently active sector. This is called when we create a new
+ * active sector.
+ *
+ * @param {String} sectorId | Id of the active sector that will be removed
+ * @private
+ */
+ exports._deleteActiveSector = function(sectorId) {
+ delete this.sectors["active"][sectorId];
+ };
+
/**
- * This function adds the child node to the parent node, creating a cluster if it is not already.
+ * This function removes the currently active sector. This is called when we reactivate
+ * the previously active sector.
*
- * @param {Node} parentNode | this is the node that will house the child node
- * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
- * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
+ * @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
- exports._addToCluster = function(parentNode, childNode, force) {
- // join child node in the parent node
- parentNode.containedNodes[childNode.id] = childNode;
-
- // manage all the edges connected to the child and parent nodes
- for (var i = 0; i < childNode.dynamicEdges.length; i++) {
- var edge = childNode.dynamicEdges[i];
- if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
- this._addToContainedEdges(parentNode,childNode,edge);
- }
- else {
- this._connectEdgeToCluster(parentNode,childNode,edge);
- }
- }
- // a contained node has no dynamic edges.
- childNode.dynamicEdges = [];
-
- // remove circular edges from clusters
- this._containCircularEdgesFromNode(parentNode,childNode);
-
-
- // remove the childNode from the global nodes object
- delete this.nodes[childNode.id];
-
- // update the properties of the child and parent
- var massBefore = parentNode.mass;
- childNode.clusterSession = this.clusterSession;
- parentNode.mass += childNode.mass;
- parentNode.clusterSize += childNode.clusterSize;
- parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
-
- // keep track of the clustersessions so we can open the cluster up as it has been formed.
- if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
- parentNode.clusterSessions.push(this.clusterSession);
- }
-
- // forced clusters only open from screen size and double tap
- if (force == true) {
- // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
- parentNode.formationScale = 0;
- }
- else {
- parentNode.formationScale = this.scale; // The latest child has been added on this scale
- }
-
- // recalculate the size of the node on the next time the node is rendered
- parentNode.clearSizeCache();
-
- // set the pop-out scale for the childnode
- parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
-
- // nullify the movement velocity of the child, this is to avoid hectic behaviour
- childNode.clearVelocity();
-
- // the mass has altered, preservation of energy dictates the velocity to be updated
- parentNode.updateVelocity(massBefore);
-
- // restart the simulation to reorganise all nodes
- this.moving = true;
- };
+ exports._deleteFrozenSector = function(sectorId) {
+ delete this.sectors["frozen"][sectorId];
+ };
/**
- * This function will apply the changes made to the remainingEdges during the formation of the clusters.
- * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
- * It has to be called if a level is collapsed. It is called by _formClusters().
+ * Freezing an active sector means moving it from the "active" object to the "frozen" object.
+ * We copy the references, then delete the active entree.
+ *
+ * @param sectorId
* @private
*/
- exports._updateDynamicEdges = function() {
- for (var i = 0; i < this.nodeIndices.length; i++) {
- var node = this.nodes[this.nodeIndices[i]];
- node.dynamicEdgesLength = node.dynamicEdges.length;
+ exports._freezeSector = function(sectorId) {
+ // we move the set references from the active to the frozen stack.
+ this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
- // this corrects for multiple edges pointing at the same other node
- var correction = 0;
- if (node.dynamicEdgesLength > 1) {
- for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
- var edgeToId = node.dynamicEdges[j].toId;
- var edgeFromId = node.dynamicEdges[j].fromId;
- for (var k = j+1; k < node.dynamicEdgesLength; k++) {
- if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
- (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
- correction += 1;
- }
- }
- }
- }
- node.dynamicEdgesLength -= correction;
- }
+ // we have moved the sector data into the frozen set, we now remove it from the active set
+ this._deleteActiveSector(sectorId);
};
/**
- * This adds an edge from the childNode to the contained edges of the parent node
+ * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
+ * object to the "active" object.
*
- * @param parentNode | Node object
- * @param childNode | Node object
- * @param edge | Edge object
+ * @param sectorId
* @private
*/
- exports._addToContainedEdges = function(parentNode, childNode, edge) {
- // create an array object if it does not yet exist for this childNode
- if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
- parentNode.containedEdges[childNode.id] = []
- }
- // add this edge to the list
- parentNode.containedEdges[childNode.id].push(edge);
-
- // remove the edge from the global edges object
- delete this.edges[edge.id];
+ exports._activateSector = function(sectorId) {
+ // we move the set references from the frozen to the active stack.
+ this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
- // remove the edge from the parent object
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- if (parentNode.dynamicEdges[i].id == edge.id) {
- parentNode.dynamicEdges.splice(i,1);
- break;
- }
- }
+ // we have moved the sector data into the active set, we now remove it from the frozen stack
+ this._deleteFrozenSector(sectorId);
};
+
/**
- * This function connects an edge that was connected to a child node to the parent node.
- * It keeps track of which nodes it has been connected to with the originalId array.
+ * This function merges the data from the currently active sector with a frozen sector. This is used
+ * in the process of reverting back to the previously active sector.
+ * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
+ * upon the creation of a new active sector.
*
- * @param {Node} parentNode | Node object
- * @param {Node} childNode | Node object
- * @param {Edge} edge | Edge object
+ * @param sectorId
* @private
*/
- exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
- // handle circular edges
- if (edge.toId == edge.fromId) {
- this._addToContainedEdges(parentNode, childNode, edge);
- }
- else {
- if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
- edge.originalToId.push(childNode.id);
- edge.to = parentNode;
- edge.toId = parentNode.id;
+ exports._mergeThisWithFrozen = function(sectorId) {
+ // copy all nodes
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
}
- else { // edge connected to other node with the "from" side
+ }
- edge.originalFromId.push(childNode.id);
- edge.from = parentNode;
- edge.fromId = parentNode.id;
+ // copy all edges (if not fully clustered, else there are no edges)
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
}
+ }
- this._addToReroutedEdges(parentNode,childNode,edge);
+ // merge the nodeIndices
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
}
};
/**
- * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
- * these edges inside of the cluster.
+ * This clusters the sector to one cluster. It was a single cluster before this process started so
+ * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
*
- * @param parentNode
- * @param childNode
* @private
*/
- exports._containCircularEdgesFromNode = function(parentNode, childNode) {
- // manage all the edges connected to the child and parent nodes
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- var edge = parentNode.dynamicEdges[i];
- // handle circular edges
- if (edge.toId == edge.fromId) {
- this._addToContainedEdges(parentNode, childNode, edge);
- }
- }
+ exports._collapseThisToSingleCluster = function() {
+ this.clusterToFit(1,false);
};
/**
- * This adds an edge from the childNode to the rerouted edges of the parent node
+ * We create a new active sector from the node that we want to open.
*
- * @param parentNode | Node object
- * @param childNode | Node object
- * @param edge | Edge object
+ * @param node
* @private
*/
- exports._addToReroutedEdges = function(parentNode, childNode, edge) {
- // create an array object if it does not yet exist for this childNode
- // we store the edge in the rerouted edges so we can restore it when the cluster pops open
- if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
- parentNode.reroutedEdges[childNode.id] = [];
- }
- parentNode.reroutedEdges[childNode.id].push(edge);
+ exports._addSector = function(node) {
+ // this is the currently active sector
+ var sector = this._sector();
- // this edge becomes part of the dynamicEdges of the cluster node
- parentNode.dynamicEdges.push(edge);
- };
+ // // this should allow me to select nodes from a frozen set.
+ // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
+ // console.log("the node is part of the active sector");
+ // }
+ // else {
+ // console.log("I dont know what the fuck happened!!");
+ // }
+ // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
+ delete this.nodes[node.id];
+ var unqiueIdentifier = util.randomUUID();
- /**
- * This function connects an edge that was connected to a cluster node back to the child node.
- *
- * @param parentNode | Node object
- * @param childNode | Node object
- * @private
- */
- exports._connectEdgeBackToChild = function(parentNode, childNode) {
- if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
- for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
- var edge = parentNode.reroutedEdges[childNode.id][i];
- if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
- edge.originalFromId.pop();
- edge.fromId = childNode.id;
- edge.from = childNode;
- }
- else {
- edge.originalToId.pop();
- edge.toId = childNode.id;
- edge.to = childNode;
- }
+ // we fully freeze the currently active sector
+ this._freezeSector(sector);
- // append this edge to the list of edges connecting to the childnode
- childNode.dynamicEdges.push(edge);
+ // we create a new active sector. This sector has the Id of the node to ensure uniqueness
+ this._createNewSector(unqiueIdentifier);
- // remove the edge from the parent object
- for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
- if (parentNode.dynamicEdges[j].id == edge.id) {
- parentNode.dynamicEdges.splice(j,1);
- break;
- }
- }
- }
- // remove the entry from the rerouted edges
- delete parentNode.reroutedEdges[childNode.id];
- }
+ // we add the active sector to the sectors array to be able to revert these steps later on
+ this._setActiveSector(unqiueIdentifier);
+
+ // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
+ this._switchToSector(this._sector());
+
+ // finally we add the node we removed from our previous active sector to the new active sector
+ this.nodes[node.id] = node;
};
/**
- * When loops are clustered, an edge can be both in the rerouted array and the contained array.
- * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
- * parentNode
+ * We close the sector that is currently open and revert back to the one before.
+ * If the active sector is the "default" sector, nothing happens.
*
- * @param parentNode | Node object
* @private
*/
- exports._validateEdges = function(parentNode) {
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- var edge = parentNode.dynamicEdges[i];
- if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
- parentNode.dynamicEdges.splice(i,1);
- }
- }
- };
+ exports._collapseSector = function() {
+ // the currently active sector
+ var sector = this._sector();
+ // we cannot collapse the default sector
+ if (sector != "default") {
+ if ((this.nodeIndices.length == 1) ||
+ (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
+ (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
+ var previousSector = this._previousSector();
- /**
- * This function released the contained edges back into the global domain and puts them back into the
- * dynamic edges of both parent and child.
- *
- * @param {Node} parentNode |
- * @param {Node} childNode |
- * @private
- */
- exports._releaseContainedEdges = function(parentNode, childNode) {
- for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
- var edge = parentNode.containedEdges[childNode.id][i];
+ // we collapse the sector back to a single cluster
+ this._collapseThisToSingleCluster();
- // put the edge back in the global edges object
- this.edges[edge.id] = edge;
+ // we move the remaining nodes, edges and nodeIndices to the previous sector.
+ // This previous sector is the one we will reactivate
+ this._mergeThisWithFrozen(previousSector);
- // put the edge back in the dynamic edges of the child and parent
- childNode.dynamicEdges.push(edge);
- parentNode.dynamicEdges.push(edge);
- }
- // remove the entry from the contained edges
- delete parentNode.containedEdges[childNode.id];
+ // the previously active (frozen) sector now has all the data from the currently active sector.
+ // we can now delete the active sector.
+ this._deleteActiveSector(sector);
- };
+ // we activate the previously active (and currently frozen) sector.
+ this._activateSector(previousSector);
+ // we load the references from the newly active sector into the global references
+ this._switchToSector(previousSector);
+ // we forget the previously active sector because we reverted to the one before
+ this._forgetLastSector();
+ // finally, we update the node index list.
+ this._updateNodeIndexList();
- // ------------------- UTILITY FUNCTIONS ---------------------------- //
+ // we refresh the list with calulation nodes and calculation node indices.
+ this._updateCalculationNodes();
+ }
+ }
+ };
/**
- * This updates the node labels for all nodes (for debugging purposes)
+ * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we dont pass the function itself because then the "this" is the window object
+ * | instead of the Network object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
*/
- exports.updateLabels = function() {
- var nodeId;
- // update node labels
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- if (node.clusterSize > 1) {
- node.label = "[".concat(String(node.clusterSize),"]");
+ exports._doInAllActiveSectors = function(runFunction,argument) {
+ if (argument === undefined) {
+ for (var sector in this.sectors["active"]) {
+ if (this.sectors["active"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToActiveSector(sector);
+ this[runFunction]();
}
}
}
-
- // update node labels
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.clusterSize == 1) {
- if (node.originalLabel !== undefined) {
- node.label = node.originalLabel;
+ else {
+ for (var sector in this.sectors["active"]) {
+ if (this.sectors["active"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToActiveSector(sector);
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
}
else {
- node.label = String(node.id);
+ this[runFunction](argument);
}
}
}
}
-
- // /* Debug Override */
- // for (nodeId in this.nodes) {
- // if (this.nodes.hasOwnProperty(nodeId)) {
- // node = this.nodes[nodeId];
- // node.label = String(node.level);
- // }
- // }
-
+ // we revert the global references back to our active sector
+ this._loadLatestSector();
};
/**
- * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
- * if the rest of the nodes are already a few cluster levels in.
- * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
- * clustered enough to the clusterToSmallestNeighbours function.
+ * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we dont pass the function itself because then the "this" is the window object
+ * | instead of the Network object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
*/
- exports.normalizeClusterLevels = function() {
- var maxLevel = 0;
- var minLevel = 1e9;
- var clusterLevel = 0;
- var nodeId;
-
- // we loop over all nodes in the list
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- clusterLevel = this.nodes[nodeId].clusterSessions.length;
- if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
- if (minLevel > clusterLevel) {minLevel = clusterLevel;}
- }
+ exports._doInSupportSector = function(runFunction,argument) {
+ if (argument === undefined) {
+ this._switchToSupportSector();
+ this[runFunction]();
}
-
- if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
- var amountOfNodes = this.nodeIndices.length;
- var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
- // we loop over all nodes in the list
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
- this._clusterToSmallestNeighbour(this.nodes[nodeId]);
- }
- }
+ else {
+ this._switchToSupportSector();
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
}
- this._updateNodeIndexList();
- this._updateDynamicEdges();
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length != amountOfNodes) {
- this.clusterSession += 1;
+ else {
+ this[runFunction](argument);
}
}
+ // we revert the global references back to our active sector
+ this._loadLatestSector();
};
-
/**
- * This function determines if the cluster we want to decluster is in the active area
- * this means around the zoom center
+ * This runs a function in all frozen sectors. This is used in the _redraw().
*
- * @param {Node} node
- * @returns {boolean}
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we don't pass the function itself because then the "this" is the window object
+ * | instead of the Network object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
- exports._nodeInActiveArea = function(node) {
- return (
- Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
- &&
- Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
- )
+ exports._doInAllFrozenSectors = function(runFunction,argument) {
+ if (argument === undefined) {
+ for (var sector in this.sectors["frozen"]) {
+ if (this.sectors["frozen"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToFrozenSector(sector);
+ this[runFunction]();
+ }
+ }
+ }
+ else {
+ for (var sector in this.sectors["frozen"]) {
+ if (this.sectors["frozen"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToFrozenSector(sector);
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
+ }
+ else {
+ this[runFunction](argument);
+ }
+ }
+ }
+ }
+ this._loadLatestSector();
};
/**
- * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
- * It puts large clusters away from the center and randomizes the order.
+ * This runs a function in all sectors. This is used in the _redraw().
*
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we don't pass the function itself because then the "this" is the window object
+ * | instead of the Network object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
*/
- exports.repositionNodes = function() {
- for (var i = 0; i < this.nodeIndices.length; i++) {
- var node = this.nodes[this.nodeIndices[i]];
- if ((node.xFixed == false || node.yFixed == false)) {
- var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
- var angle = 2 * Math.PI * Math.random();
- if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
- if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
- this._repositionBezierNodes(node);
+ exports._doInAllSectors = function(runFunction,argument) {
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (argument === undefined) {
+ this._doInAllActiveSectors(runFunction);
+ this._doInAllFrozenSectors(runFunction);
+ }
+ else {
+ if (args.length > 1) {
+ this._doInAllActiveSectors(runFunction,args[0],args[1]);
+ this._doInAllFrozenSectors(runFunction,args[0],args[1]);
+ }
+ else {
+ this._doInAllActiveSectors(runFunction,argument);
+ this._doInAllFrozenSectors(runFunction,argument);
}
}
};
/**
- * We determine how many connections denote an important hub.
- * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
+ * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
+ * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
*
* @private
*/
- exports._getHubSize = function() {
- var average = 0;
- var averageSquared = 0;
- var hubCounter = 0;
- var largestHub = 0;
-
- for (var i = 0; i < this.nodeIndices.length; i++) {
-
- var node = this.nodes[this.nodeIndices[i]];
- if (node.dynamicEdgesLength > largestHub) {
- largestHub = node.dynamicEdgesLength;
- }
- average += node.dynamicEdgesLength;
- averageSquared += Math.pow(node.dynamicEdgesLength,2);
- hubCounter += 1;
- }
- average = average / hubCounter;
- averageSquared = averageSquared / hubCounter;
-
- var variance = averageSquared - Math.pow(average,2);
-
- var standardDeviation = Math.sqrt(variance);
-
- this.hubThreshold = Math.floor(average + 2*standardDeviation);
-
- // always have at least one to cluster
- if (this.hubThreshold > largestHub) {
- this.hubThreshold = largestHub;
- }
-
- // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
- // console.log("hubThreshold:",this.hubThreshold);
- };
+ exports._clearNodeIndexList = function() {
+ var sector = this._sector();
+ this.sectors["active"][sector]["nodeIndices"] = [];
+ this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
+ };
/**
- * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
- * with this amount we can cluster specifically on these chains.
+ * Draw the encompassing sector node
*
- * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
+ * @param ctx
+ * @param sectorType
* @private
*/
- exports._reduceAmountOfChains = function(fraction) {
- this.hubThreshold = 2;
- var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
- if (reduceAmount > 0) {
- this._formClusterFromHub(this.nodes[nodeId],true,true,1);
- reduceAmount -= 1;
+ exports._drawSectorNodes = function(ctx,sectorType) {
+ var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
+ for (var sector in this.sectors[sectorType]) {
+ if (this.sectors[sectorType].hasOwnProperty(sector)) {
+ if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
+
+ this._switchToSector(sector,sectorType);
+
+ minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.resize(ctx);
+ if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
+ if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
+ if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
+ if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
+ }
}
+ node = this.sectors[sectorType][sector]["drawingNode"];
+ node.x = 0.5 * (maxX + minX);
+ node.y = 0.5 * (maxY + minY);
+ node.width = 2 * (node.x - minX);
+ node.height = 2 * (node.y - minY);
+ node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
+ node.setScale(this.scale);
+ node._drawCircle(ctx);
}
}
}
};
- /**
- * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
- * with this amount we can cluster specifically on these chains.
- *
- * @private
- */
- exports._getChainFraction = function() {
- var chains = 0;
- var total = 0;
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
- chains += 1;
- }
- total += 1;
- }
- }
- return chains/total;
+ exports._drawAllSectorNodes = function(ctx) {
+ this._drawSectorNodes(ctx,"frozen");
+ this._drawSectorNodes(ctx,"active");
+ this._loadLatestSector();
};
@@ -22611,4489 +22602,4759 @@ return /******/ (function(modules) { // webpackBootstrap
/* 44 */
/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
+ var Node = __webpack_require__(30);
/**
- * Creation of the SectorMixin var.
+ * This function can be called from the _doInAllSectors function
*
- * This contains all the functions the Network object can use to employ the sector system.
- * The sector system is always used by Network, though the benefits only apply to the use of clustering.
- * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
+ * @param object
+ * @param overlappingNodes
+ * @private
*/
+ exports._getNodesOverlappingWith = function(object, overlappingNodes) {
+ var nodes = this.nodes;
+ for (var nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ if (nodes[nodeId].isOverlappingWith(object)) {
+ overlappingNodes.push(nodeId);
+ }
+ }
+ }
+ };
/**
- * This function is only called by the setData function of the Network object.
- * This loads the global references into the active sector. This initializes the sector.
- *
+ * 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
*/
- exports._putDataInSector = function() {
- this.sectors["active"][this._sector()].nodes = this.nodes;
- this.sectors["active"][this._sector()].edges = this.edges;
- this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
+ exports._getAllNodesOverlappingWith = function (object) {
+ var overlappingNodes = [];
+ this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
+ return overlappingNodes;
};
/**
- * /**
- * This function sets the global references to nodes, edges and nodeIndices back to
- * those of the supplied (active) sector. If a type is defined, do the specific type
+ * Return a position object in canvasspace from a single point in screenspace
*
- * @param {String} sectorId
- * @param {String} [sectorType] | "active" or "frozen"
+ * @param pointer
+ * @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
- exports._switchToSector = function(sectorId, sectorType) {
- if (sectorType === undefined || sectorType == "active") {
- this._switchToActiveSector(sectorId);
- }
- else {
- this._switchToFrozenSector(sectorId);
- }
+ exports._pointerToPositionObject = function(pointer) {
+ var x = this._XconvertDOMtoCanvas(pointer.x);
+ var y = this._YconvertDOMtoCanvas(pointer.y);
+
+ return {
+ left: x,
+ top: y,
+ right: x,
+ bottom: y
+ };
};
/**
- * This function sets the global references to nodes, edges and nodeIndices back to
- * those of the supplied active sector.
+ * Get the top node at the a specific point (like a click)
*
- * @param sectorId
+ * @param {{x: Number, y: Number}} pointer
+ * @return {Node | null} node
* @private
*/
- exports._switchToActiveSector = function(sectorId) {
- this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
- this.nodes = this.sectors["active"][sectorId]["nodes"];
- this.edges = this.sectors["active"][sectorId]["edges"];
+ exports._getNodeAt = function (pointer) {
+ // we first check if this is an navigation controls element
+ var positionObject = this._pointerToPositionObject(pointer);
+ var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
+
+ // if there are overlapping nodes, select the last one, this is the
+ // one which is drawn on top of the others
+ if (overlappingNodes.length > 0) {
+ return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
+ }
+ else {
+ return null;
+ }
};
/**
- * This function sets the global references to nodes, edges and nodeIndices back to
- * those of the supplied active sector.
- *
+ * 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
*/
- exports._switchToSupportSector = function() {
- this.nodeIndices = this.sectors["support"]["nodeIndices"];
- this.nodes = this.sectors["support"]["nodes"];
- this.edges = this.sectors["support"]["edges"];
+ exports._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);
+ }
+ }
+ }
};
/**
- * This function sets the global references to nodes, edges and nodeIndices back to
- * those of the supplied frozen sector.
- *
- * @param sectorId
+ * 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
*/
- exports._switchToFrozenSector = function(sectorId) {
- this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
- this.nodes = this.sectors["frozen"][sectorId]["nodes"];
- this.edges = this.sectors["frozen"][sectorId]["edges"];
+ exports._getAllEdgesOverlappingWith = function (object) {
+ var overlappingEdges = [];
+ this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
+ return overlappingEdges;
};
-
/**
- * This function sets the global references to nodes, edges and nodeIndices back to
- * those of the currently active sector.
+ * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
+ * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
+ * @param pointer
+ * @returns {null}
* @private
*/
- exports._loadLatestSector = function() {
- this._switchToSector(this._sector());
+ exports._getEdgeAt = function(pointer) {
+ var positionObject = this._pointerToPositionObject(pointer);
+ var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
+
+ if (overlappingEdges.length > 0) {
+ return this.edges[overlappingEdges[overlappingEdges.length - 1]];
+ }
+ else {
+ return null;
+ }
};
/**
- * This function returns the currently active sector Id
+ * Add object to the selection array.
*
- * @returns {String}
+ * @param obj
* @private
*/
- exports._sector = function() {
- return this.activeSector[this.activeSector.length-1];
+ exports._addToSelection = function(obj) {
+ if (obj instanceof Node) {
+ this.selectionObj.nodes[obj.id] = obj;
+ }
+ else {
+ this.selectionObj.edges[obj.id] = obj;
+ }
};
-
/**
- * This function returns the previously active sector Id
+ * Add object to the selection array.
*
- * @returns {String}
+ * @param obj
* @private
*/
- exports._previousSector = function() {
- if (this.activeSector.length > 1) {
- return this.activeSector[this.activeSector.length-2];
+ exports._addToHover = function(obj) {
+ if (obj instanceof Node) {
+ this.hoverObj.nodes[obj.id] = obj;
}
else {
- throw new TypeError('there are not enough sectors in the this.activeSector array.');
+ this.hoverObj.edges[obj.id] = obj;
}
};
/**
- * We add the active sector at the end of the this.activeSector array
- * This ensures it is the currently active sector returned by _sector() and it reaches the top
- * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
+ * Remove a single option from selection.
*
- * @param newId
+ * @param {Object} obj
* @private
*/
- exports._setActiveSector = function(newId) {
- this.activeSector.push(newId);
- };
-
+ exports._removeFromSelection = function(obj) {
+ if (obj instanceof Node) {
+ delete this.selectionObj.nodes[obj.id];
+ }
+ else {
+ delete this.selectionObj.edges[obj.id];
+ }
+ };
/**
- * We remove the currently active sector id from the active sector stack. This happens when
- * we reactivate the previously active sector
+ * Unselect all. The selectionObj is useful for this.
*
+ * @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- exports._forgetLastSector = function() {
- this.activeSector.pop();
- };
+ exports._unselectAll = function(doNotTrigger) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ this.selectionObj.nodes[nodeId].unselect();
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ this.selectionObj.edges[edgeId].unselect();
+ }
+ }
+
+ this.selectionObj = {nodes:{},edges:{}};
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
+ };
/**
- * This function creates a new active sector with the supplied newId. This newId
- * is the expanding node id.
+ * Unselect all clusters. The selectionObj is useful for this.
*
- * @param {String} newId | Id of the new active sector
+ * @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- exports._createNewSector = function(newId) {
- // create the new sector
- this.sectors["active"][newId] = {"nodes":{},
- "edges":{},
- "nodeIndices":[],
- "formationScale": this.scale,
- "drawingNode": undefined};
+ exports._unselectClusters = function(doNotTrigger) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
- // create the new sector render node. This gives visual feedback that you are in a new sector.
- this.sectors["active"][newId]['drawingNode'] = new Node(
- {id:newId,
- color: {
- background: "#eaefef",
- border: "495c5e"
- }
- },{},{},this.constants);
- this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
+ this.selectionObj.nodes[nodeId].unselect();
+ this._removeFromSelection(this.selectionObj.nodes[nodeId]);
+ }
+ }
+ }
+
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
};
/**
- * This function removes the currently active sector. This is called when we create a new
- * active sector.
+ * return the number of selected nodes
*
- * @param {String} sectorId | Id of the active sector that will be removed
+ * @returns {number}
* @private
*/
- exports._deleteActiveSector = function(sectorId) {
- delete this.sectors["active"][sectorId];
+ exports._getSelectedNodeCount = function() {
+ var count = 0;
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
+ }
+ }
+ return count;
};
-
/**
- * This function removes the currently active sector. This is called when we reactivate
- * the previously active sector.
+ * return the selected node
*
- * @param {String} sectorId | Id of the active sector that will be removed
+ * @returns {number}
* @private
*/
- exports._deleteFrozenSector = function(sectorId) {
- delete this.sectors["frozen"][sectorId];
+ exports._getSelectedNode = function() {
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ return this.selectionObj.nodes[nodeId];
+ }
+ }
+ return null;
};
-
/**
- * Freezing an active sector means moving it from the "active" object to the "frozen" object.
- * We copy the references, then delete the active entree.
+ * return the selected edge
*
- * @param sectorId
+ * @returns {number}
* @private
*/
- exports._freezeSector = function(sectorId) {
- // we move the set references from the active to the frozen stack.
- this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
-
- // we have moved the sector data into the frozen set, we now remove it from the active set
- this._deleteActiveSector(sectorId);
+ exports._getSelectedEdge = function() {
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ return this.selectionObj.edges[edgeId];
+ }
+ }
+ return null;
};
/**
- * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
- * object to the "active" object.
+ * return the number of selected edges
*
- * @param sectorId
+ * @returns {number}
* @private
*/
- exports._activateSector = function(sectorId) {
- // we move the set references from the frozen to the active stack.
- this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
-
- // we have moved the sector data into the active set, we now remove it from the frozen stack
- this._deleteFrozenSector(sectorId);
+ exports._getSelectedEdgeCount = function() {
+ var count = 0;
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
+ }
+ }
+ return count;
};
/**
- * This function merges the data from the currently active sector with a frozen sector. This is used
- * in the process of reverting back to the previously active sector.
- * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
- * upon the creation of a new active sector.
+ * return the number of selected objects.
*
- * @param sectorId
+ * @returns {number}
* @private
*/
- exports._mergeThisWithFrozen = function(sectorId) {
- // copy all nodes
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
+ exports._getSelectedObjectCount = function() {
+ var count = 0;
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
}
}
-
- // copy all edges (if not fully clustered, else there are no edges)
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
}
}
+ return count;
+ };
- // merge the nodeIndices
- for (var i = 0; i < this.nodeIndices.length; i++) {
- this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
+ /**
+ * Check if anything is selected
+ *
+ * @returns {boolean}
+ * @private
+ */
+ exports._selectionIsEmpty = function() {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ return false;
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ return false;
+ }
}
+ return true;
};
/**
- * This clusters the sector to one cluster. It was a single cluster before this process started so
- * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
+ * check if one of the selected nodes is a cluster.
*
+ * @returns {boolean}
* @private
*/
- exports._collapseThisToSingleCluster = function() {
- this.clusterToFit(1,false);
+ exports._clusterInSelection = function() {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
+ return true;
+ }
+ }
+ }
+ return false;
};
+ /**
+ * select the edges connected to the node that is being selected
+ *
+ * @param {Node} node
+ * @private
+ */
+ exports._selectConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.select();
+ this._addToSelection(edge);
+ }
+ };
/**
- * We create a new active sector from the node that we want to open.
+ * select the edges connected to the node that is being selected
*
- * @param node
+ * @param {Node} node
* @private
*/
- exports._addSector = function(node) {
- // this is the currently active sector
- var sector = this._sector();
+ exports._hoverConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.hover = true;
+ this._addToHover(edge);
+ }
+ };
- // // this should allow me to select nodes from a frozen set.
- // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
- // console.log("the node is part of the active sector");
- // }
- // else {
- // console.log("I dont know what the fuck happened!!");
- // }
- // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
- delete this.nodes[node.id];
+ /**
+ * unselect the edges connected to the node that is being selected
+ *
+ * @param {Node} node
+ * @private
+ */
+ exports._unselectConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.unselect();
+ this._removeFromSelection(edge);
+ }
+ };
- var unqiueIdentifier = util.randomUUID();
- // we fully freeze the currently active sector
- this._freezeSector(sector);
-
- // we create a new active sector. This sector has the Id of the node to ensure uniqueness
- this._createNewSector(unqiueIdentifier);
-
- // we add the active sector to the sectors array to be able to revert these steps later on
- this._setActiveSector(unqiueIdentifier);
-
- // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
- this._switchToSector(this._sector());
-
- // finally we add the node we removed from our previous active sector to the new active sector
- this.nodes[node.id] = node;
- };
/**
- * We close the sector that is currently open and revert back to the one before.
- * If the active sector is the "default" sector, nothing happens.
+ * 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
*
+ * @param {Node || Edge} object
+ * @param {Boolean} append
+ * @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- exports._collapseSector = function() {
- // the currently active sector
- var sector = this._sector();
-
- // we cannot collapse the default sector
- if (sector != "default") {
- if ((this.nodeIndices.length == 1) ||
- (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
- (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
- var previousSector = this._previousSector();
-
- // we collapse the sector back to a single cluster
- this._collapseThisToSingleCluster();
-
- // we move the remaining nodes, edges and nodeIndices to the previous sector.
- // This previous sector is the one we will reactivate
- this._mergeThisWithFrozen(previousSector);
-
- // the previously active (frozen) sector now has all the data from the currently active sector.
- // we can now delete the active sector.
- this._deleteActiveSector(sector);
+ exports._selectObject = function(object, append, doNotTrigger, highlightEdges) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+ if (highlightEdges === undefined) {
+ highlightEdges = true;
+ }
- // we activate the previously active (and currently frozen) sector.
- this._activateSector(previousSector);
+ if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
+ this._unselectAll(true);
+ }
- // we load the references from the newly active sector into the global references
- this._switchToSector(previousSector);
+ if (object.selected == false) {
+ object.select();
+ this._addToSelection(object);
+ if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
+ this._selectConnectedEdges(object);
+ }
+ }
+ else {
+ object.unselect();
+ this._removeFromSelection(object);
+ }
- // we forget the previously active sector because we reverted to the one before
- this._forgetLastSector();
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
+ };
- // finally, we update the node index list.
- this._updateNodeIndexList();
- // we refresh the list with calulation nodes and calculation node indices.
- this._updateCalculationNodes();
- }
+ /**
+ * 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
+ *
+ * @param {Node || Edge} object
+ * @private
+ */
+ exports._blurObject = function(object) {
+ if (object.hover == true) {
+ object.hover = false;
+ this.emit("blurNode",{node:object.id});
}
};
-
/**
- * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ * 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
*
- * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
- * | we dont pass the function itself because then the "this" is the window object
- * | instead of the Network object
- * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @param {Node || Edge} object
* @private
*/
- exports._doInAllActiveSectors = function(runFunction,argument) {
- if (argument === undefined) {
- for (var sector in this.sectors["active"]) {
- if (this.sectors["active"].hasOwnProperty(sector)) {
- // switch the global references to those of this sector
- this._switchToActiveSector(sector);
- this[runFunction]();
- }
+ exports._hoverObject = function(object) {
+ if (object.hover == false) {
+ object.hover = true;
+ this._addToHover(object);
+ if (object instanceof Node) {
+ this.emit("hoverNode",{node:object.id});
}
}
- else {
- for (var sector in this.sectors["active"]) {
- if (this.sectors["active"].hasOwnProperty(sector)) {
- // switch the global references to those of this sector
- this._switchToActiveSector(sector);
- var args = Array.prototype.splice.call(arguments, 1);
- if (args.length > 1) {
- this[runFunction](args[0],args[1]);
- }
- else {
- this[runFunction](argument);
- }
- }
- }
+ if (object instanceof Node) {
+ this._hoverConnectedEdges(object);
}
- // we revert the global references back to our active sector
- this._loadLatestSector();
};
/**
- * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ * handles the selection part of the touch, only for navigation controls elements;
+ * Touch is triggered before tap, also before hold. Hold triggers after a while.
+ * This is the most responsive solution
*
- * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
- * | we dont pass the function itself because then the "this" is the window object
- * | instead of the Network object
- * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @param {Object} pointer
* @private
*/
- exports._doInSupportSector = function(runFunction,argument) {
- if (argument === undefined) {
- this._switchToSupportSector();
- this[runFunction]();
+ exports._handleTouch = function(pointer) {
+ };
+
+
+ /**
+ * handles the selection part of the tap;
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ exports._handleTap = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node,false);
}
else {
- this._switchToSupportSector();
- var args = Array.prototype.splice.call(arguments, 1);
- if (args.length > 1) {
- this[runFunction](args[0],args[1]);
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,false);
}
else {
- this[runFunction](argument);
+ this._unselectAll();
}
}
- // we revert the global references back to our active sector
- this._loadLatestSector();
+ this.emit("click", this.getSelection());
+ this._redraw();
};
/**
- * This runs a function in all frozen sectors. This is used in the _redraw().
+ * handles the selection part of the double tap and opens a cluster if needed
*
- * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
- * | we don't pass the function itself because then the "this" is the window object
- * | instead of the Network object
- * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @param {Object} pointer
* @private
*/
- exports._doInAllFrozenSectors = function(runFunction,argument) {
- if (argument === undefined) {
- for (var sector in this.sectors["frozen"]) {
- if (this.sectors["frozen"].hasOwnProperty(sector)) {
- // switch the global references to those of this sector
- this._switchToFrozenSector(sector);
- this[runFunction]();
- }
- }
- }
- else {
- for (var sector in this.sectors["frozen"]) {
- if (this.sectors["frozen"].hasOwnProperty(sector)) {
- // switch the global references to those of this sector
- this._switchToFrozenSector(sector);
- var args = Array.prototype.splice.call(arguments, 1);
- if (args.length > 1) {
- this[runFunction](args[0],args[1]);
- }
- else {
- this[runFunction](argument);
- }
- }
- }
+ exports._handleDoubleTap = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null && node !== undefined) {
+ // we reset the areaCenter here so the opening of the node will occur
+ this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
+ "y" : this._YconvertDOMtoCanvas(pointer.y)};
+ this.openCluster(node);
}
- this._loadLatestSector();
+ this.emit("doubleClick", this.getSelection());
};
/**
- * This runs a function in all sectors. This is used in the _redraw().
+ * Handle the onHold selection part
*
- * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
- * | we don't pass the function itself because then the "this" is the window object
- * | instead of the Network object
- * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @param pointer
* @private
*/
- exports._doInAllSectors = function(runFunction,argument) {
- var args = Array.prototype.splice.call(arguments, 1);
- if (argument === undefined) {
- this._doInAllActiveSectors(runFunction);
- this._doInAllFrozenSectors(runFunction);
+ exports._handleOnHold = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node,true);
}
else {
- if (args.length > 1) {
- this._doInAllActiveSectors(runFunction,args[0],args[1]);
- this._doInAllFrozenSectors(runFunction,args[0],args[1]);
- }
- else {
- this._doInAllActiveSectors(runFunction,argument);
- this._doInAllFrozenSectors(runFunction,argument);
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,true);
}
}
+ this._redraw();
};
/**
- * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
- * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
+ * handle the onRelease event. These functions are here for the navigation controls module.
*
- * @private
+ * @private
*/
- exports._clearNodeIndexList = function() {
- var sector = this._sector();
- this.sectors["active"][sector]["nodeIndices"] = [];
- this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
+ exports._handleOnRelease = function(pointer) {
+
};
+
/**
- * Draw the encompassing sector node
*
- * @param ctx
- * @param sectorType
- * @private
+ * retrieve the currently selected objects
+ * @return {{nodes: Array., edges: Array.}} selection
*/
- exports._drawSectorNodes = function(ctx,sectorType) {
- var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
- for (var sector in this.sectors[sectorType]) {
- if (this.sectors[sectorType].hasOwnProperty(sector)) {
- if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
-
- this._switchToSector(sector,sectorType);
+ exports.getSelection = function() {
+ var nodeIds = this.getSelectedNodes();
+ var edgeIds = this.getSelectedEdges();
+ return {nodes:nodeIds, edges:edgeIds};
+ };
- minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- node.resize(ctx);
- if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
- if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
- if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
- if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
- }
- }
- node = this.sectors[sectorType][sector]["drawingNode"];
- node.x = 0.5 * (maxX + minX);
- node.y = 0.5 * (maxY + minY);
- node.width = 2 * (node.x - minX);
- node.height = 2 * (node.y - minY);
- node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
- node.setScale(this.scale);
- node._drawCircle(ctx);
- }
+ /**
+ *
+ * retrieve the currently selected nodes
+ * @return {String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ exports.getSelectedNodes = function() {
+ var idArray = [];
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ idArray.push(nodeId);
}
}
+ return idArray
};
- exports._drawAllSectorNodes = function(ctx) {
- this._drawSectorNodes(ctx,"frozen");
- this._drawSectorNodes(ctx,"active");
- this._loadLatestSector();
- };
-
-
-/***/ },
-/* 45 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Node = __webpack_require__(30);
-
/**
- * This function can be called from the _doInAllSectors function
*
- * @param object
- * @param overlappingNodes
- * @private
+ * retrieve the currently selected edges
+ * @return {Array} selection An array with the ids of the
+ * selected nodes.
*/
- exports._getNodesOverlappingWith = function(object, overlappingNodes) {
- var nodes = this.nodes;
- for (var nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- if (nodes[nodeId].isOverlappingWith(object)) {
- overlappingNodes.push(nodeId);
- }
+ exports.getSelectedEdges = function() {
+ var idArray = [];
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ idArray.push(edgeId);
}
}
+ return idArray;
};
+
/**
- * 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
+ * select zero or more nodes
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
*/
- exports._getAllNodesOverlappingWith = function (object) {
- var overlappingNodes = [];
- this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
- return overlappingNodes;
+ exports.setSelection = function(selection) {
+ var i, iMax, id;
+
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
+ }
+ this._selectObject(node,true,true);
+ }
+
+ console.log("setSelection is deprecated. Please use selectNodes instead.")
+
+ this.redraw();
};
/**
- * Return a position object in canvasspace from a single point in screenspace
- *
- * @param pointer
- * @returns {{left: number, top: number, right: number, bottom: number}}
- * @private
+ * select zero or more nodes with the option to highlight edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ * @param {boolean} [highlightEdges]
*/
- exports._pointerToPositionObject = function(pointer) {
- var x = this._XconvertDOMtoCanvas(pointer.x);
- var y = this._YconvertDOMtoCanvas(pointer.y);
+ exports.selectNodes = function(selection, highlightEdges) {
+ var i, iMax, id;
- return {
- left: x,
- top: y,
- right: x,
- bottom: y
- };
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
+ }
+ this._selectObject(node,true,true,highlightEdges);
+ }
+ this.redraw();
};
/**
- * Get the top node at the a specific point (like a click)
- *
- * @param {{x: Number, y: Number}} pointer
- * @return {Node | null} node
- * @private
+ * select zero or more edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
*/
- exports._getNodeAt = function (pointer) {
- // we first check if this is an navigation controls element
- var positionObject = this._pointerToPositionObject(pointer);
- var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
+ exports.selectEdges = function(selection) {
+ var i, iMax, id;
- // if there are overlapping nodes, select the last one, this is the
- // one which is drawn on top of the others
- if (overlappingNodes.length > 0) {
- return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
- }
- else {
- return null;
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var edge = this.edges[id];
+ if (!edge) {
+ throw new RangeError('Edge with id "' + id + '" not found');
+ }
+ this._selectObject(edge,true,true,highlightEdges);
}
+ this.redraw();
};
-
/**
- * 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
+ * Validate the selection: remove ids of nodes which no longer exist
* @private
*/
- exports._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);
+ exports._updateSelection = function () {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (!this.nodes.hasOwnProperty(nodeId)) {
+ delete this.selectionObj.nodes[nodeId];
+ }
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ if (!this.edges.hasOwnProperty(edgeId)) {
+ delete this.selectionObj.edges[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
- */
- exports._getAllEdgesOverlappingWith = function (object) {
- var overlappingEdges = [];
- this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
- return overlappingEdges;
- };
+/***/ },
+/* 45 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var Node = __webpack_require__(30);
+ var Edge = __webpack_require__(27);
/**
- * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
- * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
+ * clears the toolbar div element of children
*
- * @param pointer
- * @returns {null}
* @private
*/
- exports._getEdgeAt = function(pointer) {
- var positionObject = this._pointerToPositionObject(pointer);
- var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
-
- if (overlappingEdges.length > 0) {
- return this.edges[overlappingEdges[overlappingEdges.length - 1]];
- }
- else {
- return null;
+ exports._clearManipulatorBar = function() {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
};
-
/**
- * Add object to the selection array.
+ * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
+ * these functions to their original functionality, we saved them in this.cachedFunctions.
+ * This function restores these functions to their original function.
*
- * @param obj
* @private
*/
- exports._addToSelection = function(obj) {
- if (obj instanceof Node) {
- this.selectionObj.nodes[obj.id] = obj;
- }
- else {
- this.selectionObj.edges[obj.id] = obj;
+ exports._restoreOverloadedFunctions = function() {
+ for (var functionName in this.cachedFunctions) {
+ if (this.cachedFunctions.hasOwnProperty(functionName)) {
+ this[functionName] = this.cachedFunctions[functionName];
+ }
}
};
/**
- * Add object to the selection array.
+ * Enable or disable edit-mode.
*
- * @param obj
* @private
*/
- exports._addToHover = function(obj) {
- if (obj instanceof Node) {
- this.hoverObj.nodes[obj.id] = obj;
+ exports._toggleEditMode = function() {
+ this.editMode = !this.editMode;
+ var toolbar = document.getElementById("network-manipulationDiv");
+ var closeDiv = document.getElementById("network-manipulation-closeDiv");
+ var editModeDiv = document.getElementById("network-manipulation-editMode");
+ if (this.editMode == true) {
+ toolbar.style.display="block";
+ closeDiv.style.display="block";
+ editModeDiv.style.display="none";
+ closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
- this.hoverObj.edges[obj.id] = obj;
+ toolbar.style.display="none";
+ closeDiv.style.display="none";
+ editModeDiv.style.display="block";
+ closeDiv.onclick = null;
}
+ this._createManipulatorBar()
};
-
/**
- * Remove a single option from selection.
+ * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
- * @param {Object} obj
* @private
*/
- exports._removeFromSelection = function(obj) {
- if (obj instanceof Node) {
- delete this.selectionObj.nodes[obj.id];
- }
- else {
- delete this.selectionObj.edges[obj.id];
+ exports._createManipulatorBar = function() {
+ // remove bound functions
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
}
- };
- /**
- * Unselect all. The selectionObj is useful for this.
- *
- * @param {Boolean} [doNotTrigger] | ignore trigger
- * @private
- */
- exports._unselectAll = function(doNotTrigger) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
+ if (this.edgeBeingEdited !== undefined) {
+ this.edgeBeingEdited._disableControlNodes();
+ this.edgeBeingEdited = undefined;
+ this.selectedControlNode = null;
+ this.controlNodesActive = false;
}
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- this.selectionObj.nodes[nodeId].unselect();
+
+ // restore overloaded functions
+ this._restoreOverloadedFunctions();
+
+ // resume calculation
+ this.freezeSimulation = false;
+
+ // reset global variables
+ this.blockConnectingEdgeSelection = false;
+ this.forceAppendSelection = false;
+
+ if (this.editMode == true) {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
- }
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- this.selectionObj.edges[edgeId].unselect();
+ // add the icons to the manipulator div
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ ""+this.constants.labels['add'] +"" +
+ "" +
+ "" +
+ ""+this.constants.labels['link'] +"";
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['editNode'] +"";
+ }
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['editEdge'] +"";
+ }
+ if (this._selectionIsEmpty() == false) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['del'] +"";
}
- }
- this.selectionObj = {nodes:{},edges:{}};
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
+ // bind the icons
+ var addNodeButton = document.getElementById("network-manipulate-addNode");
+ addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
+ var addEdgeButton = document.getElementById("network-manipulate-connectNode");
+ addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ var editButton = document.getElementById("network-manipulate-editNode");
+ editButton.onclick = this._editNode.bind(this);
+ }
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ var editButton = document.getElementById("network-manipulate-editEdge");
+ editButton.onclick = this._createEditEdgeToolbar.bind(this);
+ }
+ if (this._selectionIsEmpty() == false) {
+ var deleteButton = document.getElementById("network-manipulate-delete");
+ deleteButton.onclick = this._deleteSelected.bind(this);
+ }
+ var closeDiv = document.getElementById("network-manipulation-closeDiv");
+ closeDiv.onclick = this._toggleEditMode.bind(this);
+
+ this.boundFunction = this._createManipulatorBar.bind(this);
+ this.on('select', this.boundFunction);
+ }
+ else {
+ this.editModeDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['edit'] + "";
+ var editModeButton = document.getElementById("network-manipulate-editModeButton");
+ editModeButton.onclick = this._toggleEditMode.bind(this);
}
};
+
+
/**
- * Unselect all clusters. The selectionObj is useful for this.
+ * Create the toolbar for adding Nodes
*
- * @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- exports._unselectClusters = function(doNotTrigger) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
+ exports._createAddNodeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
}
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
- this.selectionObj.nodes[nodeId].unselect();
- this._removeFromSelection(this.selectionObj.nodes[nodeId]);
- }
- }
- }
+ // create the toolbar contents
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['addDescription'] + "";
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
- }
+ // bind the icon
+ var backButton = document.getElementById("network-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);
};
/**
- * return the number of selected nodes
+ * create the toolbar to connect nodes
*
- * @returns {number}
* @private
*/
- exports._getSelectedNodeCount = function() {
- var count = 0;
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
- }
- return count;
- };
+ exports._createAddEdgeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ this._unselectAll(true);
+ this.freezeSimulation = true;
- /**
- * return the selected node
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedNode = function() {
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- return this.selectionObj.nodes[nodeId];
- }
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
}
- return null;
- };
- /**
- * return the selected edge
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedEdge = function() {
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return this.selectionObj.edges[edgeId];
- }
- }
- return null;
- };
+ this._unselectAll();
+ this.forceAppendSelection = false;
+ this.blockConnectingEdgeSelection = true;
+
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['linkDescription'] + "";
+
+ // bind the icon
+ var backButton = document.getElementById("network-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._handleConnect.bind(this);
+ 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;
+
+ // redraw to show the unselect
+ this._redraw();
+ };
/**
- * return the number of selected edges
+ * create the toolbar to edit edges
*
- * @returns {number}
* @private
*/
- exports._getSelectedEdgeCount = function() {
- var count = 0;
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
- }
+ exports._createEditEdgeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ this.controlNodesActive = true;
+
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
}
- return count;
- };
+
+ this.edgeBeingEdited = this._getSelectedEdge();
+ this.edgeBeingEdited._enableControlNodes();
+
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['editEdgeDescription'] + "";
+
+ // bind the icon
+ var backButton = document.getElementById("network-manipulate-back");
+ backButton.onclick = this._createManipulatorBar.bind(this);
+
+ // temporarily overload functions
+ this.cachedFunctions["_handleTouch"] = this._handleTouch;
+ this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
+ this.cachedFunctions["_handleTap"] = this._handleTap;
+ this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
+ this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
+ this._handleTouch = this._selectControlNode;
+ this._handleTap = function () {};
+ this._handleOnDrag = this._controlNodeDrag;
+ this._handleDragStart = function () {}
+ this._handleOnRelease = this._releaseControlNode;
+
+ // redraw to show the unselect
+ this._redraw();
+ };
+
+
+
/**
- * return the number of selected objects.
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @returns {number}
* @private
*/
- exports._getSelectedObjectCount = function() {
- var count = 0;
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
- }
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
- }
+ exports._selectControlNode = function(pointer) {
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ this.edgeBeingEdited.controlNodes.to.unselect();
+ this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
+ if (this.selectedControlNode !== null) {
+ this.selectedControlNode.select();
+ this.freezeSimulation = true;
}
- return count;
+ this._redraw();
};
/**
- * Check if anything is selected
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @returns {boolean}
* @private
*/
- exports._selectionIsEmpty = function() {
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- return false;
- }
+ exports._controlNodeDrag = function(event) {
+ var pointer = this._getPointer(event.gesture.center);
+ if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
+ this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
+ this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
}
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return false;
+ this._redraw();
+ };
+
+ exports._releaseControlNode = function(pointer) {
+ var newNode = this._getNodeAt(pointer);
+ if (newNode != null) {
+ if (this.edgeBeingEdited.controlNodes.from.selected == true) {
+ this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ }
+ if (this.edgeBeingEdited.controlNodes.to.selected == true) {
+ this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
+ this.edgeBeingEdited.controlNodes.to.unselect();
}
}
- return true;
+ else {
+ this.edgeBeingEdited._restoreControlNodes();
+ }
+ this.freezeSimulation = false;
+ this._redraw();
};
-
/**
- * check if one of the selected nodes is a cluster.
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @returns {boolean}
* @private
*/
- exports._clusterInSelection = function() {
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
- return true;
+ exports._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.center);
+ this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
+ this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
+ this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
+ this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
+ };
+
+ this.moving = true;
+ this.start();
}
}
}
- return false;
};
- /**
- * select the edges connected to the node that is being selected
- *
- * @param {Node} node
- * @private
- */
- exports._selectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.select();
- this._addToSelection(edge);
+ exports._finishConnect = function(pointer) {
+ if (this._getSelectedNodeCount() == 1) {
+
+ // restore the drag function
+ this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
+ delete this.cachedFunctions["_handleOnDrag"];
+
+ // remember the edge id
+ var connectFromId = this.edges['connectionEdge'].fromId;
+
+ // 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();
}
};
+
/**
- * select the edges connected to the node that is being selected
- *
- * @param {Node} node
- * @private
+ * Adds a node on the specified location
*/
- exports._hoverConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.hover = true;
- this._addToHover(edge);
+ exports._addNode = function() {
+ if (this._selectionIsEmpty() && this.editMode == true) {
+ var positionObject = this._pointerToPositionObject(this.pointerPosition);
+ var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
+ if (this.triggerFunctions.add) {
+ if (this.triggerFunctions.add.length == 2) {
+ var me = this;
+ this.triggerFunctions.add(defaultData, function(finalizedData) {
+ me.nodesData.add(finalizedData);
+ me._createManipulatorBar();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels['addError']);
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.nodesData.add(defaultData);
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
}
};
/**
- * unselect the edges connected to the node that is being selected
+ * connect two nodes with a new edge.
*
- * @param {Node} node
* @private
*/
- exports._unselectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.unselect();
- this._removeFromSelection(edge);
+ exports._createEdge = function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.connect) {
+ if (this.triggerFunctions.connect.length == 2) {
+ var me = this;
+ this.triggerFunctions.connect(defaultData, function(finalizedData) {
+ me.edgesData.add(finalizedData);
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["linkError"]);
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.edgesData.add(defaultData);
+ this.moving = true;
+ this.start();
+ }
}
};
-
-
-
/**
- * 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
+ * connect two nodes with a new edge.
*
- * @param {Node || Edge} object
- * @param {Boolean} append
- * @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- exports._selectObject = function(object, append, doNotTrigger, highlightEdges) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
- }
- if (highlightEdges === undefined) {
- highlightEdges = true;
- }
-
- if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
- this._unselectAll(true);
+ exports._editEdge = function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.editEdge) {
+ if (this.triggerFunctions.editEdge.length == 2) {
+ var me = this;
+ this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
+ me.edgesData.update(finalizedData);
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["linkError"]);
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.edgesData.update(defaultData);
+ this.moving = true;
+ this.start();
+ }
}
+ };
- if (object.selected == false) {
- object.select();
- this._addToSelection(object);
- if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
- this._selectConnectedEdges(object);
+ /**
+ * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
+ *
+ * @private
+ */
+ exports._editNode = function() {
+ if (this.triggerFunctions.edit && this.editMode == true) {
+ var node = this._getSelectedNode();
+ var data = {id:node.id,
+ label: node.label,
+ group: node.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(this.constants.labels["editError"]);
}
}
else {
- object.unselect();
- this._removeFromSelection(object);
- }
-
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
+ alert(this.constants.labels["editBoundError"]);
}
};
+
+
/**
- * 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
+ * delete everything in the selection
*
- * @param {Node || Edge} object
* @private
*/
- exports._blurObject = function(object) {
- if (object.hover == true) {
- object.hover = false;
- this.emit("blurNode",{node:object.id});
+ exports._deleteSelected = function() {
+ if (!this._selectionIsEmpty() && this.editMode == true) {
+ if (!this._clusterInSelection()) {
+ var selectedNodes = this.getSelectedNodes();
+ var selectedEdges = this.getSelectedEdges();
+ if (this.triggerFunctions.del) {
+ var me = this;
+ var data = {nodes: selectedNodes, edges: selectedEdges};
+ if (this.triggerFunctions.del.length = 2) {
+ this.triggerFunctions.del(data, function (finalizedData) {
+ me.edgesData.remove(finalizedData.edges);
+ me.nodesData.remove(finalizedData.nodes);
+ me._unselectAll();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["deleteError"])
+ }
+ }
+ else {
+ this.edgesData.remove(selectedEdges);
+ this.nodesData.remove(selectedNodes);
+ this._unselectAll();
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ alert(this.constants.labels["deleteClusterError"]);
+ }
+ }
+ };
+
+
+/***/ },
+/* 46 */
+/***/ function(module, exports, __webpack_require__) {
+
+ exports._cleanNavigation = function() {
+ // clean up previous navigation items
+ var wrapper = document.getElementById('network-navigation_wrapper');
+ if (wrapper != null) {
+ this.containerElement.removeChild(wrapper);
}
+ document.onmouseup = null;
};
/**
- * 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
+ * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
+ * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
+ * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
+ * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
*
- * @param {Node || Edge} object
* @private
*/
- exports._hoverObject = function(object) {
- if (object.hover == false) {
- object.hover = true;
- this._addToHover(object);
- if (object instanceof Node) {
- this.emit("hoverNode",{node:object.id});
- }
- }
- if (object instanceof Node) {
- this._hoverConnectedEdges(object);
+ exports._loadNavigationElements = function() {
+ this._cleanNavigation();
+
+ this.navigationDivs = {};
+ var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
+ var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
+
+ this.navigationDivs['wrapper'] = document.createElement('div');
+ this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
+ this.navigationDivs['wrapper'].style.position = "absolute";
+ this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
+ this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
+ this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
+
+ for (var i = 0; i < navigationDivs.length; i++) {
+ this.navigationDivs[navigationDivs[i]] = document.createElement('div');
+ this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
+ this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
+ this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
+ this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
- };
+ document.onmouseup = this._stopMovement.bind(this);
+ };
/**
- * handles the selection part of the touch, only for navigation controls elements;
- * Touch is triggered before tap, also before hold. Hold triggers after a while.
- * This is the most responsive solution
+ * this stops all movement induced by the navigation buttons
*
- * @param {Object} pointer
* @private
*/
- exports._handleTouch = function(pointer) {
+ exports._stopMovement = function() {
+ this._xStopMoving();
+ this._yStopMoving();
+ this._stopZoom();
};
/**
- * handles the selection part of the tap;
+ * stops the actions performed by page up and down etc.
*
- * @param {Object} pointer
+ * @param event
* @private
*/
- exports._handleTap = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null) {
- this._selectObject(node,false);
- }
- else {
- var edge = this._getEdgeAt(pointer);
- if (edge != null) {
- this._selectObject(edge,false);
- }
- else {
- this._unselectAll();
+ exports._preventDefault = function(event) {
+ if (event !== undefined) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
}
}
- this.emit("click", this.getSelection());
- this._redraw();
};
/**
- * handles the selection part of the double tap and opens a cluster if needed
+ * move the screen up
+ * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
+ * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
+ * To avoid this behaviour, we do the translation in the start loop.
*
- * @param {Object} pointer
* @private
*/
- exports._handleDoubleTap = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null && node !== undefined) {
- // we reset the areaCenter here so the opening of the node will occur
- this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
- "y" : this._YconvertDOMtoCanvas(pointer.y)};
- this.openCluster(node);
+ exports._moveUp = function(event) {
+ this.yIncrement = this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['up'].className += " active";
}
- this.emit("doubleClick", this.getSelection());
};
/**
- * Handle the onHold selection part
- *
- * @param pointer
+ * move the screen down
* @private
*/
- exports._handleOnHold = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null) {
- this._selectObject(node,true);
- }
- else {
- var edge = this._getEdgeAt(pointer);
- if (edge != null) {
- this._selectObject(edge,true);
- }
+ exports._moveDown = function(event) {
+ this.yIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['down'].className += " active";
}
- this._redraw();
};
/**
- * handle the onRelease event. These functions are here for the navigation controls module.
- *
- * @private
+ * move the screen left
+ * @private
*/
- exports._handleOnRelease = function(pointer) {
-
- };
-
+ exports._moveLeft = function(event) {
+ this.xIncrement = this.constants.keyboard.speed.x;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['left'].className += " active";
+ }
+ };
/**
- *
- * retrieve the currently selected objects
- * @return {{nodes: Array., edges: Array.}} selection
+ * move the screen right
+ * @private
*/
- exports.getSelection = function() {
- var nodeIds = this.getSelectedNodes();
- var edgeIds = this.getSelectedEdges();
- return {nodes:nodeIds, edges:edgeIds};
+ exports._moveRight = function(event) {
+ this.xIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['right'].className += " active";
+ }
};
+
/**
- *
- * retrieve the currently selected nodes
- * @return {String[]} selection An array with the ids of the
- * selected nodes.
+ * Zoom in, using the same method as the movement.
+ * @private
*/
- exports.getSelectedNodes = function() {
- var idArray = [];
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- idArray.push(nodeId);
- }
+ exports._zoomIn = function(event) {
+ this.zoomIncrement = this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomIn'].className += " active";
}
- return idArray
};
+
/**
- *
- * retrieve the currently selected edges
- * @return {Array} selection An array with the ids of the
- * selected nodes.
+ * Zoom out
+ * @private
*/
- exports.getSelectedEdges = function() {
- var idArray = [];
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- idArray.push(edgeId);
- }
+ exports._zoomOut = function() {
+ this.zoomIncrement = -this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomOut'].className += " active";
}
- return idArray;
};
/**
- * select zero or more nodes
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
+ * Stop zooming and unhighlight the zoom controls
+ * @private
*/
- exports.setSelection = function(selection) {
- var i, iMax, id;
-
- if (!selection || (selection.length == undefined))
- throw 'Selection must be an array with ids';
-
- // first unselect any selected node
- this._unselectAll(true);
-
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
-
- var node = this.nodes[id];
- if (!node) {
- throw new RangeError('Node with id "' + id + '" not found');
- }
- this._selectObject(node,true,true);
+ exports._stopZoom = function() {
+ this.zoomIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
+ this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
}
-
- console.log("setSelection is deprecated. Please use selectNodes instead.")
-
- this.redraw();
};
/**
- * select zero or more nodes with the option to highlight edges
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
- * @param {boolean} [highlightEdges]
+ * Stop moving in the Y direction and unHighlight the up and down
+ * @private
*/
- exports.selectNodes = function(selection, highlightEdges) {
- var i, iMax, id;
-
- if (!selection || (selection.length == undefined))
- throw 'Selection must be an array with ids';
-
- // first unselect any selected node
- this._unselectAll(true);
-
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
-
- var node = this.nodes[id];
- if (!node) {
- throw new RangeError('Node with id "' + id + '" not found');
- }
- this._selectObject(node,true,true,highlightEdges);
+ exports._yStopMoving = function() {
+ this.yIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
+ this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
}
- this.redraw();
};
/**
- * select zero or more edges
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
+ * Stop moving in the X direction and unHighlight left and right.
+ * @private
*/
- exports.selectEdges = function(selection) {
- var i, iMax, id;
-
- if (!selection || (selection.length == undefined))
- throw 'Selection must be an array with ids';
+ exports._xStopMoving = function() {
+ this.xIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
+ this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
+ }
+ };
- // first unselect any selected node
- this._unselectAll(true);
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
+/***/ },
+/* 47 */
+/***/ function(module, exports, __webpack_require__) {
- var edge = this.edges[id];
- if (!edge) {
- throw new RangeError('Edge with id "' + id + '" not found');
+ exports._resetLevels = function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.preassignedLevel == false) {
+ node.level = -1;
+ }
}
- this._selectObject(edge,true,true,highlightEdges);
}
- this.redraw();
};
/**
- * Validate the selection: remove ids of nodes which no longer exist
+ * This is the main function to layout the nodes in a hierarchical way.
+ * It checks if the node details are supplied correctly
+ *
* @private
*/
- exports._updateSelection = function () {
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (!this.nodes.hasOwnProperty(nodeId)) {
- delete this.selectionObj.nodes[nodeId];
+ exports._setupHierarchicalLayout = function() {
+ if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
+ this.constants.hierarchicalLayout.levelSeparation *= -1;
+ }
+ else {
+ this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
+ }
+
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "vertical";
}
}
- }
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- if (!this.edges.hasOwnProperty(edgeId)) {
- delete this.selectionObj.edges[edgeId];
+ else {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "horizontal";
}
}
- }
- };
+ // get the size of the largest hubs and check if the user has defined a level for a node.
+ var hubsize = 0;
+ var node, nodeId;
+ var definedLevel = false;
+ var undefinedLevel = false;
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level != -1) {
+ definedLevel = true;
+ }
+ else {
+ undefinedLevel = true;
+ }
+ if (hubsize < node.edges.length) {
+ hubsize = node.edges.length;
+ }
+ }
+ }
-/***/ },
-/* 46 */
-/***/ function(module, exports, __webpack_require__) {
+ // if the user defined some levels but not all, alert and run without hierarchical layout
+ if (undefinedLevel == true && definedLevel == true) {
+ alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
+ this.zoomExtent(true,this.constants.clustering.enabled);
+ if (!this.constants.clustering.enabled) {
+ this.start();
+ }
+ }
+ else {
+ // setup the system to use hierarchical method.
+ this._changeConstants();
- var util = __webpack_require__(1);
- var Node = __webpack_require__(30);
- var Edge = __webpack_require__(27);
+ // define levels if undefined by the users. Based on hubsize
+ if (undefinedLevel == true) {
+ this._determineLevels(hubsize);
+ }
+ // check the distribution of the nodes per level.
+ var distribution = this._getDistribution();
- /**
- * clears the toolbar div element of children
- *
- * @private
- */
- exports._clearManipulatorBar = function() {
- while (this.manipulationDiv.hasChildNodes()) {
- this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
- }
- };
+ // place the nodes on the canvas. This also stablilizes the system.
+ this._placeNodesByHierarchy(distribution);
- /**
- * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
- * these functions to their original functionality, we saved them in this.cachedFunctions.
- * This function restores these functions to their original function.
- *
- * @private
- */
- exports._restoreOverloadedFunctions = function() {
- for (var functionName in this.cachedFunctions) {
- if (this.cachedFunctions.hasOwnProperty(functionName)) {
- this[functionName] = this.cachedFunctions[functionName];
+ // start the simulation.
+ this.start();
}
}
};
+
/**
- * Enable or disable edit-mode.
+ * This function places the nodes on the canvas based on the hierarchial distribution.
*
+ * @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
- exports._toggleEditMode = function() {
- this.editMode = !this.editMode;
- var toolbar = document.getElementById("network-manipulationDiv");
- var closeDiv = document.getElementById("network-manipulation-closeDiv");
- var editModeDiv = document.getElementById("network-manipulation-editMode");
- if (this.editMode == true) {
- toolbar.style.display="block";
- closeDiv.style.display="block";
- editModeDiv.style.display="none";
- closeDiv.onclick = this._toggleEditMode.bind(this);
- }
- else {
- toolbar.style.display="none";
- closeDiv.style.display="none";
- editModeDiv.style.display="block";
- closeDiv.onclick = null;
+ exports._placeNodesByHierarchy = function(distribution) {
+ var nodeId, node;
+
+ // start placing all the level 0 nodes first. Then recursively position their branches.
+ for (nodeId in distribution[0].nodes) {
+ if (distribution[0].nodes.hasOwnProperty(nodeId)) {
+ node = distribution[0].nodes[nodeId];
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (node.xFixed) {
+ node.x = distribution[0].minPos;
+ node.xFixed = false;
+
+ distribution[0].minPos += distribution[0].nodeSpacing;
+ }
+ }
+ else {
+ if (node.yFixed) {
+ node.y = distribution[0].minPos;
+ node.yFixed = false;
+
+ distribution[0].minPos += distribution[0].nodeSpacing;
+ }
+ }
+ this._placeBranchNodes(node.edges,node.id,distribution,node.level);
+ }
}
- this._createManipulatorBar()
+
+ // stabilize the system after positioning. This function calls zoomExtent.
+ this._stabilize();
};
+
/**
- * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
+ * This function get the distribution of levels based on hubsize
*
+ * @returns {Object}
* @private
*/
- exports._createManipulatorBar = function() {
- // remove bound functions
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
- }
- if (this.edgeBeingEdited !== undefined) {
- this.edgeBeingEdited._disableControlNodes();
- this.edgeBeingEdited = undefined;
- this.selectedControlNode = null;
+ exports._getDistribution = function() {
+ var distribution = {};
+ var nodeId, node, level;
+
+ // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
+ // the fix of X is removed after the x value has been set.
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.xFixed = true;
+ node.yFixed = true;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ else {
+ node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ if (!distribution.hasOwnProperty(node.level)) {
+ distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
+ }
+ distribution[node.level].amount += 1;
+ distribution[node.level].nodes[node.id] = node;
+ }
}
- // restore overloaded functions
- this._restoreOverloadedFunctions();
+ // determine the largest amount of nodes of all levels
+ var maxCount = 0;
+ for (level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ if (maxCount < distribution[level].amount) {
+ maxCount = distribution[level].amount;
+ }
+ }
+ }
- // resume calculation
- this.freezeSimulation = false;
+ // set the initial position and spacing of each nodes accordingly
+ for (level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
+ distribution[level].nodeSpacing /= (distribution[level].amount + 1);
+ distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
+ }
+ }
- // reset global variables
- this.blockConnectingEdgeSelection = false;
- this.forceAppendSelection = false;
+ return distribution;
+ };
- if (this.editMode == true) {
- while (this.manipulationDiv.hasChildNodes()) {
- this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
- }
- // add the icons to the manipulator div
- this.manipulationDiv.innerHTML = "" +
- "" +
- ""+this.constants.labels['add'] +"" +
- "" +
- "" +
- ""+this.constants.labels['link'] +"";
- if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
- this.manipulationDiv.innerHTML += "" +
- "" +
- "" +
- ""+this.constants.labels['editNode'] +"";
- }
- else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
- this.manipulationDiv.innerHTML += "" +
- "" +
- "" +
- ""+this.constants.labels['editEdge'] +"";
- }
- if (this._selectionIsEmpty() == false) {
- this.manipulationDiv.innerHTML += "" +
- "" +
- "" +
- ""+this.constants.labels['del'] +"";
- }
+ /**
+ * this function allocates nodes in levels based on the recursive branching from the largest hubs.
+ *
+ * @param hubsize
+ * @private
+ */
+ exports._determineLevels = function(hubsize) {
+ var nodeId, node;
- // bind the icons
- var addNodeButton = document.getElementById("network-manipulate-addNode");
- addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
- var addEdgeButton = document.getElementById("network-manipulate-connectNode");
- addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
- if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
- var editButton = document.getElementById("network-manipulate-editNode");
- editButton.onclick = this._editNode.bind(this);
- }
- else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
- var editButton = document.getElementById("network-manipulate-editEdge");
- editButton.onclick = this._createEditEdgeToolbar.bind(this);
- }
- if (this._selectionIsEmpty() == false) {
- var deleteButton = document.getElementById("network-manipulate-delete");
- deleteButton.onclick = this._deleteSelected.bind(this);
+ // determine hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.edges.length == hubsize) {
+ node.level = 0;
+ }
}
- var closeDiv = document.getElementById("network-manipulation-closeDiv");
- closeDiv.onclick = this._toggleEditMode.bind(this);
-
- this.boundFunction = this._createManipulatorBar.bind(this);
- this.on('select', this.boundFunction);
}
- else {
- this.editModeDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['edit'] + "";
- var editModeButton = document.getElementById("network-manipulate-editModeButton");
- editModeButton.onclick = this._toggleEditMode.bind(this);
+
+ // branch from hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level == 0) {
+ this._setLevel(1,node.edges,node.id);
+ }
+ }
}
};
-
/**
- * Create the toolbar for adding Nodes
+ * Since hierarchical layout does not support:
+ * - smooth curves (based on the physics),
+ * - clustering (based on dynamic node counts)
+ *
+ * We disable both features so there will be no problems.
*
* @private
*/
- exports._createAddNodeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
+ exports._changeConstants = function() {
+ this.constants.clustering.enabled = false;
+ this.constants.physics.barnesHut.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this._loadSelectedForceSolver();
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.dynamic = false;
}
-
- // create the toolbar contents
- this.manipulationDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['back'] + " " +
- "" +
- "" +
- "" + this.constants.labels['addDescription'] + "";
-
- // bind the icon
- var backButton = document.getElementById("network-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);
+ this._configureSmoothCurves();
};
/**
- * create the toolbar to connect nodes
+ * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
+ * on a X position that ensures there will be no overlap.
*
+ * @param edges
+ * @param parentId
+ * @param distribution
+ * @param parentLevel
* @private
*/
- exports._createAddEdgeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
- this._unselectAll(true);
- this.freezeSimulation = true;
-
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
- }
-
- this._unselectAll();
- this.forceAppendSelection = false;
- this.blockConnectingEdgeSelection = true;
+ exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
+ }
+ else {
+ childNode = edges[i].to;
+ }
- this.manipulationDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['back'] + " " +
- "" +
- "" +
- "" + this.constants.labels['linkDescription'] + "";
-
- // bind the icon
- var backButton = document.getElementById("network-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._handleConnect.bind(this);
- 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;
-
- // redraw to show the unselect
- this._redraw();
- };
-
- /**
- * create the toolbar to edit edges
- *
- * @private
- */
- exports._createEditEdgeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
+ // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
+ var nodeMoved = false;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (childNode.xFixed && childNode.level > parentLevel) {
+ childNode.xFixed = false;
+ childNode.x = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
+ else {
+ if (childNode.yFixed && childNode.level > parentLevel) {
+ childNode.yFixed = false;
+ childNode.y = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
+ if (nodeMoved == true) {
+ distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
+ if (childNode.edges.length > 1) {
+ this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
+ }
+ }
}
-
- this.edgeBeingEdited = this._getSelectedEdge();
- this.edgeBeingEdited._enableControlNodes();
-
- this.manipulationDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['back'] + " " +
- "" +
- "" +
- "" + this.constants.labels['editEdgeDescription'] + "";
-
- // bind the icon
- var backButton = document.getElementById("network-manipulate-back");
- backButton.onclick = this._createManipulatorBar.bind(this);
-
- // temporarily overload functions
- this.cachedFunctions["_handleTouch"] = this._handleTouch;
- this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
- this.cachedFunctions["_handleTap"] = this._handleTap;
- this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
- this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
- this._handleTouch = this._selectControlNode;
- this._handleTap = function () {};
- this._handleOnDrag = this._controlNodeDrag;
- this._handleDragStart = function () {}
- this._handleOnRelease = this._releaseControlNode;
-
- // redraw to show the unselect
- this._redraw();
};
-
-
-
- /**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
- *
- * @private
- */
- exports._selectControlNode = function(pointer) {
- this.edgeBeingEdited.controlNodes.from.unselect();
- this.edgeBeingEdited.controlNodes.to.unselect();
- this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
- if (this.selectedControlNode !== null) {
- this.selectedControlNode.select();
- this.freezeSimulation = true;
- }
- this._redraw();
- };
-
/**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
+ * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
+ * @param level
+ * @param edges
+ * @param parentId
* @private
*/
- exports._controlNodeDrag = function(event) {
- var pointer = this._getPointer(event.gesture.center);
- if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
- this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
- this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
- }
- this._redraw();
- };
-
- exports._releaseControlNode = function(pointer) {
- var newNode = this._getNodeAt(pointer);
- if (newNode != null) {
- if (this.edgeBeingEdited.controlNodes.from.selected == true) {
- this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
- this.edgeBeingEdited.controlNodes.from.unselect();
+ exports._setLevel = function(level, edges, parentId) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
}
- if (this.edgeBeingEdited.controlNodes.to.selected == true) {
- this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
- this.edgeBeingEdited.controlNodes.to.unselect();
+ else {
+ childNode = edges[i].to;
+ }
+ if (childNode.level == -1 || childNode.level > level) {
+ childNode.level = level;
+ if (edges.length > 1) {
+ this._setLevel(level+1, childNode.edges, childNode.id);
+ }
}
}
- else {
- this.edgeBeingEdited._restoreControlNodes();
- }
- this.freezeSimulation = false;
- this._redraw();
};
+
/**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
+ * Unfix nodes
*
* @private
*/
- exports._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.center);
- this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
- this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
- this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
- this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
- };
-
- this.moving = true;
- this.start();
- }
+ exports._restoreNodes = function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId].xFixed = false;
+ this.nodes[nodeId].yFixed = false;
}
}
};
- exports._finishConnect = function(pointer) {
- if (this._getSelectedNodeCount() == 1) {
-
- // restore the drag function
- this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
- delete this.cachedFunctions["_handleOnDrag"];
-
- // remember the edge id
- var connectFromId = this.edges['connectionEdge'].fromId;
-
- // 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();
- }
- };
+/***/ },
+/* 48 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Adds a node on the specified location
+ * Copyright 2012 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.1.2
+ * @url craig.is/killing/mice
*/
- exports._addNode = function() {
- if (this._selectionIsEmpty() && this.editMode == true) {
- var positionObject = this._pointerToPositionObject(this.pointerPosition);
- var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
- if (this.triggerFunctions.add) {
- if (this.triggerFunctions.add.length == 2) {
- var me = this;
- this.triggerFunctions.add(defaultData, function(finalizedData) {
- me.nodesData.add(finalizedData);
- me._createManipulatorBar();
- me.moving = true;
- me.start();
- });
- }
- else {
- alert(this.constants.labels['addError']);
- this._createManipulatorBar();
- this.moving = true;
- this.start();
- }
- }
- else {
- this.nodesData.add(defaultData);
- this._createManipulatorBar();
- this.moving = true;
- this.start();
- }
- }
- };
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ },
- /**
- * connect two nodes with a new edge.
- *
- * @private
- */
- exports._createEdge = function(sourceNodeId,targetNodeId) {
- if (this.editMode == true) {
- var defaultData = {from:sourceNodeId, to:targetNodeId};
- if (this.triggerFunctions.connect) {
- if (this.triggerFunctions.connect.length == 2) {
- var me = this;
- this.triggerFunctions.connect(defaultData, function(finalizedData) {
- me.edgesData.add(finalizedData);
- me.moving = true;
- me.start();
- });
- }
- else {
- alert(this.constants.labels["linkError"]);
- this.moving = true;
- this.start();
- }
- }
- else {
- this.edgesData.add(defaultData);
- this.moving = true;
- this.start();
- }
- }
- };
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
- /**
- * connect two nodes with a new edge.
- *
- * @private
- */
- exports._editEdge = function(sourceNodeId,targetNodeId) {
- if (this.editMode == true) {
- var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
- if (this.triggerFunctions.editEdge) {
- if (this.triggerFunctions.editEdge.length == 2) {
- var me = this;
- this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
- me.edgesData.update(finalizedData);
- me.moving = true;
- me.start();
- });
- }
- else {
- alert(this.constants.labels["linkError"]);
- this.moving = true;
- this.start();
- }
- }
- else {
- this.edgesData.update(defaultData);
- this.moving = true;
- this.start();
- }
- }
- };
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ },
- /**
- * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
- *
- * @private
- */
- exports._editNode = function() {
- if (this.triggerFunctions.edit && this.editMode == true) {
- var node = this._getSelectedNode();
- var data = {id:node.id,
- label: node.label,
- group: node.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(this.constants.labels["editError"]);
- }
- }
- else {
- alert(this.constants.labels["editBoundError"]);
- }
- };
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc'
+ },
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ _REVERSE_MAP,
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ _callbacks = {},
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ _direct_map = {},
- /**
- * delete everything in the selection
- *
- * @private
- */
- exports._deleteSelected = function() {
- if (!this._selectionIsEmpty() && this.editMode == true) {
- if (!this._clusterInSelection()) {
- var selectedNodes = this.getSelectedNodes();
- var selectedEdges = this.getSelectedEdges();
- if (this.triggerFunctions.del) {
- var me = this;
- var data = {nodes: selectedNodes, edges: selectedEdges};
- if (this.triggerFunctions.del.length = 2) {
- this.triggerFunctions.del(data, function (finalizedData) {
- me.edgesData.remove(finalizedData.edges);
- me.nodesData.remove(finalizedData.nodes);
- me._unselectAll();
- me.moving = true;
- me.start();
- });
- }
- else {
- alert(this.constants.labels["deleteError"])
- }
- }
- else {
- this.edgesData.remove(selectedEdges);
- this.nodesData.remove(selectedNodes);
- this._unselectAll();
- this.moving = true;
- this.start();
- }
- }
- else {
- alert(this.constants.labels["deleteClusterError"]);
- }
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ _sequence_levels = {},
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ _reset_timer,
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ _ignore_next_keyup = false,
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ _inside_sequence = false;
+
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
}
- };
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
-/***/ },
-/* 47 */
-/***/ function(module, exports, __webpack_require__) {
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ return object.addEventListener(type, callback, false);
+ }
- exports._cleanNavigation = function() {
- // clean up previous navigation items
- var wrapper = document.getElementById('network-navigation_wrapper');
- if (wrapper != null) {
- this.containerElement.removeChild(wrapper);
+ object.attachEvent('on' + type, callback);
}
- document.onmouseup = null;
- };
- /**
- * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
- * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
- * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
- * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
- *
- * @private
- */
- exports._loadNavigationElements = function() {
- this._cleanNavigation();
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
- this.navigationDivs = {};
- var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
- var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ return String.fromCharCode(e.which);
+ }
- this.navigationDivs['wrapper'] = document.createElement('div');
- this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
- this.navigationDivs['wrapper'].style.position = "absolute";
- this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
- this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
- this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
- for (var i = 0; i < navigationDivs.length; i++) {
- this.navigationDivs[navigationDivs[i]] = document.createElement('div');
- this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
- this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
- this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
- this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
+
+ // if it is not in the special map
+ return String.fromCharCode(e.which).toLowerCase();
}
- document.onmouseup = this._stopMovement.bind(this);
- };
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @return {boolean}
+ */
+ function _stop(e) {
+ var element = e.target || e.srcElement,
+ tag_name = element.tagName;
- /**
- * this stops all movement induced by the navigation buttons
- *
- * @private
- */
- exports._stopMovement = function() {
- this._xStopMoving();
- this._yStopMoving();
- this._stopZoom();
- };
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+ // stop for input, select, and textarea
+ return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ }
- /**
- * stops the actions performed by page up and down etc.
- *
- * @param event
- * @private
- */
- exports._preventDefault = function(event) {
- if (event !== undefined) {
- if (event.preventDefault) {
- event.preventDefault();
- } else {
- event.returnValue = false;
- }
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
- };
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} do_not_reset
+ * @returns void
+ */
+ function _resetSequences(do_not_reset) {
+ do_not_reset = do_not_reset || {};
- /**
- * move the screen up
- * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
- * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
- * To avoid this behaviour, we do the translation in the start loop.
- *
- * @private
- */
- exports._moveUp = function(event) {
- this.yIncrement = this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['up'].className += " active";
- }
- };
+ var active_sequences = false,
+ key;
+ for (key in _sequence_levels) {
+ if (do_not_reset[key]) {
+ active_sequences = true;
+ continue;
+ }
+ _sequence_levels[key] = 0;
+ }
- /**
- * move the screen down
- * @private
- */
- exports._moveDown = function(event) {
- this.yIncrement = -this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['down'].className += " active";
+ if (!active_sequences) {
+ _inside_sequence = false;
+ }
}
- };
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {string} action
+ * @param {boolean=} remove - should we remove any matches
+ * @param {string=} combination
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, action, remove, combination) {
+ var i,
+ callback,
+ matches = [];
+
+ // if there are no events related to this keycode
+ if (!_callbacks[character]) {
+ return [];
+ }
- /**
- * move the screen left
- * @private
- */
- exports._moveLeft = function(event) {
- this.xIncrement = this.constants.keyboard.speed.x;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['left'].className += " active";
- }
- };
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < _callbacks[character].length; ++i) {
+ callback = _callbacks[character][i];
- /**
- * move the screen right
- * @private
- */
- exports._moveRight = function(event) {
- this.xIncrement = -this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['right'].className += " active";
- }
- };
+ // if this is a sequence but it is not at the right level
+ // then move onto the next match
+ if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
+ continue;
+ }
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
- /**
- * Zoom in, using the same method as the movement.
- * @private
- */
- exports._zoomIn = function(event) {
- this.zoomIncrement = this.constants.keyboard.speed.zoom;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['zoomIn'].className += " active";
- }
- };
+ // if this is a keypress event that means that we need to only
+ // look at the character, otherwise check the modifiers as
+ // well
+ if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
+ // remove is used so if you change your mind and call bind a
+ // second time with a new function the first one is overwritten
+ if (remove && callback.combo == combination) {
+ _callbacks[character].splice(i, 1);
+ }
- /**
- * Zoom out
- * @private
- */
- exports._zoomOut = function() {
- this.zoomIncrement = -this.constants.keyboard.speed.zoom;
- this.start(); // if there is no node movement, the calculation wont be done
- this._preventDefault(event);
- if (this.navigationDivs) {
- this.navigationDivs['zoomOut'].className += " active";
+ matches.push(callback);
+ }
+ }
+
+ return matches;
}
- };
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
- /**
- * Stop zooming and unhighlight the zoom controls
- * @private
- */
- exports._stopZoom = function() {
- this.zoomIncrement = 0;
- if (this.navigationDivs) {
- this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
- this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
- }
- };
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
- /**
- * Stop moving in the Y direction and unHighlight the up and down
- * @private
- */
- exports._yStopMoving = function() {
- this.yIncrement = 0;
- if (this.navigationDivs) {
- this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
- this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
- }
- };
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
- /**
- * Stop moving in the X direction and unHighlight left and right.
- * @private
- */
- exports._xStopMoving = function() {
- this.xIncrement = 0;
- if (this.navigationDivs) {
- this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
- this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
+ return modifiers;
}
- };
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e) {
+ if (callback(e) === false) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
-/***/ },
-/* 48 */
-/***/ function(module, exports, __webpack_require__) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
- exports._resetLevels = function() {
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- if (node.preassignedLevel == false) {
- node.level = -1;
+ e.returnValue = false;
+ e.cancelBubble = true;
}
- }
}
- };
- /**
- * This is the main function to layout the nodes in a hierarchical way.
- * It checks if the node details are supplied correctly
- *
- * @private
- */
- exports._setupHierarchicalLayout = function() {
- if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
- if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
- this.constants.hierarchicalLayout.levelSeparation *= -1;
- }
- else {
- this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
- }
- // get the size of the largest hubs and check if the user has defined a level for a node.
- var hubsize = 0;
- var node, nodeId;
- var definedLevel = false;
- var undefinedLevel = false;
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleCharacter(character, e) {
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.level != -1) {
- definedLevel = true;
- }
- else {
- undefinedLevel = true;
- }
- if (hubsize < node.edges.length) {
- hubsize = node.edges.length;
- }
+ // if this event should not happen stop here
+ if (_stop(e)) {
+ return;
}
- }
- // if the user defined some levels but not all, alert and run without hierarchical layout
- if (undefinedLevel == true && definedLevel == true) {
- alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
- this.zoomExtent(true,this.constants.clustering.enabled);
- if (!this.constants.clustering.enabled) {
- this.start();
- }
- }
- else {
- // setup the system to use hierarchical method.
- this._changeConstants();
+ var callbacks = _getMatches(character, _eventModifiers(e), e.type),
+ i,
+ do_not_reset = {},
+ processed_sequence_callback = false;
- // define levels if undefined by the users. Based on hubsize
- if (undefinedLevel == true) {
- this._determineLevels(hubsize);
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+ processed_sequence_callback = true;
+
+ // keep a list of which sequences were matches for later
+ do_not_reset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e);
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processed_sequence_callback && !_inside_sequence) {
+ _fireCallback(callbacks[i].callback, e);
+ }
}
- // check the distribution of the nodes per level.
- var distribution = this._getDistribution();
- // place the nodes on the canvas. This also stablilizes the system.
- this._placeNodesByHierarchy(distribution);
-
- // start the simulation.
- this.start();
- }
+ // if you are inside of a sequence and the key you are pressing
+ // is not a modifier key then we should reset all sequences
+ // that were not matched by this key event
+ if (e.type == _inside_sequence && !_isModifier(character)) {
+ _resetSequences(do_not_reset);
+ }
}
- };
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKey(e) {
- /**
- * This function places the nodes on the canvas based on the hierarchial distribution.
- *
- * @param {Object} distribution | obtained by the function this._getDistribution()
- * @private
- */
- exports._placeNodesByHierarchy = function(distribution) {
- var nodeId, node;
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ e.which = typeof e.which == "number" ? e.which : e.keyCode;
- // start placing all the level 0 nodes first. Then recursively position their branches.
- for (nodeId in distribution[0].nodes) {
- if (distribution[0].nodes.hasOwnProperty(nodeId)) {
- node = distribution[0].nodes[nodeId];
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (node.xFixed) {
- node.x = distribution[0].minPos;
- node.xFixed = false;
+ var character = _characterFromEvent(e);
- distribution[0].minPos += distribution[0].nodeSpacing;
- }
+ // no character found then stop
+ if (!character) {
+ return;
}
- else {
- if (node.yFixed) {
- node.y = distribution[0].minPos;
- node.yFixed = false;
- distribution[0].minPos += distribution[0].nodeSpacing;
- }
+ if (e.type == 'keyup' && _ignore_next_keyup == character) {
+ _ignore_next_keyup = false;
+ return;
}
- this._placeBranchNodes(node.edges,node.id,distribution,node.level);
- }
+
+ _handleCharacter(character, e);
}
- // stabilize the system after positioning. This function calls zoomExtent.
- this._stabilize();
- };
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ }
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_reset_timer);
+ _reset_timer = setTimeout(_resetSequences, 1000);
+ }
- /**
- * This function get the distribution of levels based on hubsize
- *
- * @returns {Object}
- * @private
- */
- exports._getDistribution = function() {
- var distribution = {};
- var nodeId, node, level;
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
- // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
- // the fix of X is removed after the x value has been set.
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- node.xFixed = true;
- node.yFixed = true;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
}
- else {
- node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ return _REVERSE_MAP;
+ }
+
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
+
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
}
- if (!distribution.hasOwnProperty(node.level)) {
- distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
+
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
}
- distribution[node.level].amount += 1;
- distribution[node.level].nodes[node.id] = node;
- }
+
+ return action;
}
- // determine the largest amount of nodes of all levels
- var maxCount = 0;
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- if (maxCount < distribution[level].amount) {
- maxCount = distribution[level].amount;
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
+
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequence_levels[combo] = 0;
+
+ // if there is no action pick the best one for the first key
+ // in the sequence
+ if (!action) {
+ action = _pickBestAction(keys[0], []);
}
- }
- }
- // set the initial position and spacing of each nodes accordingly
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
- distribution[level].nodeSpacing /= (distribution[level].amount + 1);
- distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
- }
- }
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ var _increaseSequence = function(e) {
+ _inside_sequence = action;
+ ++_sequence_levels[combo];
+ _resetSequenceTimer();
+ },
- return distribution;
- };
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ _callbackAndReset = function(e) {
+ _fireCallback(callback, e);
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignore_next_keyup = _characterFromEvent(e);
+ }
- /**
- * this function allocates nodes in levels based on the recursive branching from the largest hubs.
- *
- * @param hubsize
- * @private
- */
- exports._determineLevels = function(hubsize) {
- var nodeId, node;
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ },
+ i;
- // determine hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.edges.length == hubsize) {
- node.level = 0;
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ for (i = 0; i < keys.length; ++i) {
+ _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
}
- }
}
- // branch from hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.level == 0) {
- this._setLevel(1,node.edges,node.id);
- }
- }
- }
- };
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequence_name - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequence_name, level) {
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' '),
+ i,
+ key,
+ keys,
+ modifiers = [];
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ return _bindSequence(combination, sequence, callback, action);
+ }
- /**
- * Since hierarchical layout does not support:
- * - smooth curves (based on the physics),
- * - clustering (based on dynamic node counts)
- *
- * We disable both features so there will be no problems.
- *
- * @private
- */
- exports._changeConstants = function() {
- this.constants.clustering.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this._loadSelectedForceSolver();
- this.constants.smoothCurves = false;
- this._configureSmoothCurves();
- };
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = combination === '+' ? ['+'] : combination.split('+');
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
- /**
- * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
- * on a X position that ensures there will be no overlap.
- *
- * @param edges
- * @param parentId
- * @param distribution
- * @param parentLevel
- * @private
- */
- exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- }
- else {
- childNode = edges[i].to;
- }
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
- // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
- var nodeMoved = false;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (childNode.xFixed && childNode.level > parentLevel) {
- childNode.xFixed = false;
- childNode.x = distribution[childNode.level].minPos;
- nodeMoved = true;
- }
- }
- else {
- if (childNode.yFixed && childNode.level > parentLevel) {
- childNode.yFixed = false;
- childNode.y = distribution[childNode.level].minPos;
- nodeMoved = true;
- }
- }
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
- if (nodeMoved == true) {
- distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
- if (childNode.edges.length > 1) {
- this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
}
- }
- }
- };
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
- /**
- * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
- *
- * @param level
- * @param edges
- * @param parentId
- * @private
- */
- exports._setLevel = function(level, edges, parentId) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- }
- else {
- childNode = edges[i].to;
- }
- if (childNode.level == -1 || childNode.level > level) {
- childNode.level = level;
- if (edges.length > 1) {
- this._setLevel(level+1, childNode.edges, childNode.id);
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ if (!_callbacks[key]) {
+ _callbacks[key] = [];
}
- }
- }
- };
+ // remove an existing match if there is one
+ _getMatches(key, modifiers, action, !sequence_name, combination);
- /**
- * Unfix nodes
- *
- * @private
- */
- exports._restoreNodes = function() {
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.nodes[nodeId].xFixed = false;
- this.nodes[nodeId].yFixed = false;
- }
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ _callbacks[key][sequence_name ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: modifiers,
+ action: action,
+ seq: sequence_name,
+ level: level,
+ combo: combination
+ });
}
- };
-
-/***/ },
-/* 49 */
-/***/ function(module, exports, __webpack_require__) {
-
- /*! Hammer.JS - v1.0.5 - 2013-04-07
- * http://eightmedia.github.com/hammer.js
- *
- * Copyright (c) 2013 Jorik Tangelder ;
- * Licensed under the MIT license */
-
- (function(window, undefined) {
- 'use strict';
-
- /**
- * Hammer
- * use this to create instances
- * @param {HTMLElement} element
- * @param {Object} options
- * @returns {Hammer.Instance}
- * @constructor
- */
- var Hammer = function(element, options) {
- return new Hammer.Instance(element, options || {});
- };
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ function _bindMultiple(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ }
- // default settings
- Hammer.defaults = {
- // add styles and attributes to the element to prevent the browser from doing
- // its native behavior. this doesnt prevent the scrolling, but cancels
- // the contextmenu, tap highlighting etc
- // set to false to disable this
- stop_browser_behavior: {
- // this also triggers onselectstart=false for IE
- userSelect: 'none',
- // this makes the element blocking in IE10 >, you could experiment with the value
- // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
- touchAction: 'none',
- touchCallout: 'none',
- contentZooming: 'none',
- userDrag: 'none',
- tapHighlightColor: 'rgba(0,0,0,0)'
- }
+ // start!
+ _addEvent(document, 'keypress', _handleKey);
+ _addEvent(document, 'keydown', _handleKey);
+ _addEvent(document, 'keyup', _handleKey);
- // more settings are defined per gesture at gestures.js
- };
+ var mousetrap = {
- // detect touchevents
- Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
- Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * a comma separated list of keys, an array of keys, or
+ * a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ bind: function(keys, callback, action) {
+ _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
+ _direct_map[keys + ':' + action] = callback;
+ return this;
+ },
- // dont use mouseevents on mobile devices
- Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
- Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _direct_map dict.
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ unbind: function(keys, action) {
+ if (_direct_map[keys + ':' + action]) {
+ delete _direct_map[keys + ':' + action];
+ this.bind(keys, function() {}, action);
+ }
+ return this;
+ },
- // eventtypes per touchevent (start, move, end)
- // are filled by Hammer.event.determineEventTypes on setup
- Hammer.EVENT_TYPES = {};
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ trigger: function(keys, action) {
+ _direct_map[keys + ':' + action]();
+ return this;
+ },
- // direction defines
- Hammer.DIRECTION_DOWN = 'down';
- Hammer.DIRECTION_LEFT = 'left';
- Hammer.DIRECTION_UP = 'up';
- Hammer.DIRECTION_RIGHT = 'right';
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ reset: function() {
+ _callbacks = {};
+ _direct_map = {};
+ return this;
+ }
+ };
- // pointer type
- Hammer.POINTER_MOUSE = 'mouse';
- Hammer.POINTER_TOUCH = 'touch';
- Hammer.POINTER_PEN = 'pen';
+ module.exports = mousetrap;
- // touch event defines
- Hammer.EVENT_START = 'start';
- Hammer.EVENT_MOVE = 'move';
- Hammer.EVENT_END = 'end';
- // hammer document where the base events are added at
- Hammer.DOCUMENT = document;
- // plugins namespace
- Hammer.plugins = {};
+/***/ },
+/* 49 */
+/***/ function(module, exports, __webpack_require__) {
- // if the window events are set...
- Hammer.READY = false;
+ var util = __webpack_require__(1);
+ var RepulsionMixin = __webpack_require__(52);
+ var HierarchialRepulsionMixin = __webpack_require__(53);
+ var BarnesHutMixin = __webpack_require__(54);
/**
- * setup events to detect gestures on the document
+ * Toggling barnes Hut calculation on and off.
+ *
+ * @private
*/
- function setup() {
- if(Hammer.READY) {
- return;
- }
-
- // find what eventtypes we add listeners to
- Hammer.event.determineEventTypes();
-
- // Register all gestures inside Hammer.gestures
- for(var name in Hammer.gestures) {
- if(Hammer.gestures.hasOwnProperty(name)) {
- Hammer.detection.register(Hammer.gestures[name]);
- }
- }
-
- // Add touch events on the document
- Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
- Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
+ exports._toggleBarnesHut = function () {
+ this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
+ this._loadSelectedForceSolver();
+ this.moving = true;
+ this.start();
+ };
- // Hammer is ready...!
- Hammer.READY = true;
- }
/**
- * create new hammer instance
- * all methods should return the instance itself, so it is chainable.
- * @param {HTMLElement} element
- * @param {Object} [options={}]
- * @returns {Hammer.Instance}
- * @constructor
+ * This loads the node force solver based on the barnes hut or repulsion algorithm
+ *
+ * @private
*/
- Hammer.Instance = function(element, options) {
- var self = this;
-
- // setup HammerJS window events and register all gestures
- // this also sets up the default options
- setup();
+ exports._loadSelectedForceSolver = function () {
+ // this overloads the this._calculateNodeForces
+ if (this.constants.physics.barnesHut.enabled == true) {
+ this._clearMixin(RepulsionMixin);
+ this._clearMixin(HierarchialRepulsionMixin);
- this.element = element;
+ this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
+ this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
+ this.constants.physics.damping = this.constants.physics.barnesHut.damping;
- // start/stop detection option
- this.enabled = true;
+ this._loadMixin(BarnesHutMixin);
+ }
+ else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._clearMixin(BarnesHutMixin);
+ this._clearMixin(RepulsionMixin);
- // merge options
- this.options = Hammer.utils.extend(
- Hammer.utils.extend({}, Hammer.defaults),
- options || {});
+ this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
- // add some css to the element to prevent the browser from doing its native behavoir
- if(this.options.stop_browser_behavior) {
- Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
- }
+ this._loadMixin(HierarchialRepulsionMixin);
+ }
+ else {
+ this._clearMixin(BarnesHutMixin);
+ this._clearMixin(HierarchialRepulsionMixin);
+ this.barnesHutTree = undefined;
- // start detection on touchstart
- Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
- if(self.enabled) {
- Hammer.detection.startDetect(self, ev);
- }
- });
+ this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.repulsion.damping;
- // return instance
- return this;
+ this._loadMixin(RepulsionMixin);
+ }
};
+ /**
+ * Before calculating the forces, we check if we need to cluster to keep up performance and we check
+ * if there is more than one node. If it is just one node, we dont calculate anything.
+ *
+ * @private
+ */
+ exports._initializeForceCalculation = function () {
+ // stop calculation if there is only one node
+ if (this.nodeIndices.length == 1) {
+ this.nodes[this.nodeIndices[0]]._setForce(0, 0);
+ }
+ else {
+ // if there are too many nodes on screen, we cluster without repositioning
+ if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
+ this.clusterToFit(this.constants.clustering.reduceToNodes, false);
+ }
- Hammer.Instance.prototype = {
- /**
- * bind events to the instance
- * @param {String} gesture
- * @param {Function} handler
- * @returns {Hammer.Instance}
- */
- on: function onEvent(gesture, handler){
- var gestures = gesture.split(' ');
- for(var t=0; t 0) {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this._calculateSpringForcesWithSupport();
+ }
+ else {
+ if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._calculateHierarchicalSpringForces();
+ }
+ else {
+ this._calculateSpringForces();
+ }
}
+ }
};
+
/**
- * this holds the last move event,
- * used to fix empty touchend issue
- * see the onTouch event for an explanation
- * @type {Object}
+ * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
+ * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
+ * This function joins the datanodes and invisible (called support) nodes into one object.
+ * We do this so we do not contaminate this.nodes with the support nodes.
+ *
+ * @private
*/
- var last_move_event = null;
+ exports._updateCalculationNodes = function () {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId] = this.nodes[nodeId];
+ }
+ }
+ var supportNodes = this.sectors['support']['nodes'];
+ for (var supportNodeId in supportNodes) {
+ if (supportNodes.hasOwnProperty(supportNodeId)) {
+ if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
+ this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
+ }
+ else {
+ supportNodes[supportNodeId]._setForce(0, 0);
+ }
+ }
+ }
- /**
- * when the mouse is hold down, this is true
- * @type {Boolean}
- */
- var enable_detect = false;
+ for (var idx in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(idx)) {
+ this.calculationNodeIndices.push(idx);
+ }
+ }
+ }
+ else {
+ this.calculationNodes = this.nodes;
+ this.calculationNodeIndices = this.nodeIndices;
+ }
+ };
/**
- * when touch events have been fired, this is true
- * @type {Boolean}
+ * this function applies the central gravity effect to keep groups from floating off
+ *
+ * @private
*/
- var touch_triggered = false;
-
-
- Hammer.event = {
- /**
- * simple addEventListener
- * @param {HTMLElement} element
- * @param {String} type
- * @param {Function} handler
- */
- bindDom: function(element, type, handler) {
- var types = type.split(' ');
- for(var t=0; t 0 && eventType == Hammer.EVENT_END) {
- eventType = Hammer.EVENT_MOVE;
- }
- // no touches, force the end event
- else if(!count_touches) {
- eventType = Hammer.EVENT_END;
- }
+ if (distance == 0) {
+ distance = 0.01;
+ }
- // because touchend has no touches, and we often want to use these in our gestures,
- // we send the last move event as our eventData in touchend
- if(!count_touches && last_move_event !== null) {
- ev = last_move_event;
- }
- // store the last move event
- else {
- last_move_event = ev;
- }
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
- // trigger the handler
- handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
+ fx = dx * springForce;
+ fy = dy * springForce;
- // remove pointerevent from list
- if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
- count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
- }
- }
+ edge.from.fx += fx;
+ edge.from.fy += fy;
+ edge.to.fx -= fx;
+ edge.to.fy -= fy;
+ }
+ }
+ }
+ }
+ };
- //debug(sourceEventType +" "+ eventType);
- // on the end we reset everything
- if(!count_touches) {
- last_move_event = null;
- enable_detect = false;
- touch_triggered = false;
- Hammer.PointerEvent.reset();
- }
- });
- },
- /**
- * we have different events for each device/browser
- * determine what we need and set them in the Hammer.EVENT_TYPES constant
- */
- determineEventTypes: function determineEventTypes() {
- // determine the eventtype we want to set
- var types;
+ /**
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ exports._calculateSpringForcesWithSupport = function () {
+ var edgeLength, edge, edgeId, combinedClusterSize;
+ var edges = this.edges;
- // pointerEvents magic
- if(Hammer.HAS_POINTEREVENTS) {
- types = Hammer.PointerEvent.getEvents();
- }
- // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
- else if(Hammer.NO_MOUSEEVENTS) {
- types = [
- 'touchstart',
- 'touchmove',
- 'touchend touchcancel'];
- }
- // for non pointer events browsers and mixed browsers,
- // like chrome on windows8 touch laptop
- else {
- types = [
- 'touchstart mousedown',
- 'touchmove mousemove',
- 'touchend touchcancel mouseup'];
- }
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
- Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
- Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
- Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
- },
+ edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
+ combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
- /**
- * create touchlist depending on the event
- * @param {Object} ev
- * @param {String} eventType used by the fakemultitouch plugin
- */
- getTouchList: function getTouchList(ev/*, eventType*/) {
- // get the fake pointerEvent touchlist
- if(Hammer.HAS_POINTEREVENTS) {
- return Hammer.PointerEvent.getTouchList();
- }
- // get the touchlist
- else if(ev.touches) {
- return ev.touches;
- }
- // make fake touchlist from mouse position
- else {
- return [{
- identifier: 1,
- pageX: ev.pageX,
- pageY: ev.pageY,
- target: ev.target
- }];
+ // this implies that the edges between big clusters are longer
+ edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
+ this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
+ this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ }
}
- },
+ }
+ }
+ }
+ };
- /**
- * collect event data for Hammer js
- * @param {HTMLElement} element
- * @param {String} eventType like Hammer.EVENT_MOVE
- * @param {Object} eventData
- */
- collectEventData: function collectEventData(element, eventType, ev) {
- var touches = this.getTouchList(ev, eventType);
+ /**
+ * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
+ *
+ * @param node1
+ * @param node2
+ * @param edgeLength
+ * @private
+ */
+ exports._calculateSpringForce = function (node1, node2, edgeLength) {
+ var dx, dy, fx, fy, springForce, distance;
- // find out pointerType
- var pointerType = Hammer.POINTER_TOUCH;
- if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
- pointerType = Hammer.POINTER_MOUSE;
- }
+ dx = (node1.x - node2.x);
+ dy = (node1.y - node2.y);
+ distance = Math.sqrt(dx * dx + dy * dy);
- return {
- center : Hammer.utils.getCenter(touches),
- timeStamp : new Date().getTime(),
- target : ev.target,
- touches : touches,
- eventType : eventType,
- pointerType : pointerType,
- srcEvent : ev,
+ if (distance == 0) {
+ distance = 0.01;
+ }
- /**
- * prevent the browser default actions
- * mostly used to disable scrolling of the browser
- */
- preventDefault: function() {
- if(this.srcEvent.preventManipulation) {
- this.srcEvent.preventManipulation();
- }
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
- if(this.srcEvent.preventDefault) {
- this.srcEvent.preventDefault();
- }
- },
+ fx = dx * springForce;
+ fy = dy * springForce;
- /**
- * stop bubbling the event up to its parents
- */
- stopPropagation: function() {
- this.srcEvent.stopPropagation();
- },
+ node1.fx += fx;
+ node1.fy += fy;
+ node2.fx -= fx;
+ node2.fy -= fy;
+ };
- /**
- * immediately stop gesture detection
- * might be useful after a swipe was detected
- * @return {*}
- */
- stopDetect: function() {
- return Hammer.detection.stopDetect();
- }
- };
+
+ /**
+ * Load the HTML for the physics config and bind it
+ * @private
+ */
+ exports._loadPhysicsConfiguration = function () {
+ if (this.physicsConfiguration === undefined) {
+ this.backupConstants = {};
+ util.deepExtend(this.backupConstants,this.constants);
+
+ var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
+ this.physicsConfiguration = document.createElement('div');
+ this.physicsConfiguration.className = "PhysicsConfiguration";
+ this.physicsConfiguration.innerHTML = '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ ''
+ this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
+ this.optionsDiv = document.createElement("div");
+ this.optionsDiv.style.fontSize = "14px";
+ this.optionsDiv.style.fontFamily = "verdana";
+ this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
+
+ var rangeElement;
+ rangeElement = document.getElementById('graph_BH_gc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
+ rangeElement = document.getElementById('graph_BH_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_BH_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_BH_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_BH_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
+
+ rangeElement = document.getElementById('graph_R_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_R_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_R_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_R_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_R_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
+
+ rangeElement = document.getElementById('graph_H_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_H_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_H_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_H_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_H_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
+ rangeElement = document.getElementById('graph_H_direction');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
+ rangeElement = document.getElementById('graph_H_levsep');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
+ rangeElement = document.getElementById('graph_H_nspac');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
+
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ var radioButton3 = document.getElementById("graph_physicsMethod3");
+ radioButton2.checked = true;
+ if (this.constants.physics.barnesHut.enabled) {
+ radioButton1.checked = true;
+ }
+ if (this.constants.hierarchicalLayout.enabled) {
+ radioButton3.checked = true;
}
- };
- Hammer.PointerEvent = {
- /**
- * holds all pointers
- * @type {Object}
- */
- pointers: {},
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ var graph_repositionNodes = document.getElementById("graph_repositionNodes");
+ var graph_generateOptions = document.getElementById("graph_generateOptions");
- /**
- * get a list of pointers
- * @returns {Array} touchlist
- */
- getTouchList: function() {
- var self = this;
- var touchlist = [];
+ graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
+ graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
+ graph_generateOptions.onclick = graphGenerateOptions.bind(this);
+ if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
+ graph_toggleSmooth.style.background = "#A4FF56";
+ }
+ else {
+ graph_toggleSmooth.style.background = "#FF8532";
+ }
- // we can use forEach since pointerEvents only is in IE10
- Object.keys(self.pointers).sort().forEach(function(id) {
- touchlist.push(self.pointers[id]);
- });
- return touchlist;
- },
- /**
- * update the position of a pointer
- * @param {String} type Hammer.EVENT_END
- * @param {Object} pointerEvent
- */
- updatePointer: function(type, pointerEvent) {
- if(type == Hammer.EVENT_END) {
- this.pointers = {};
- }
- else {
- pointerEvent.identifier = pointerEvent.pointerId;
- this.pointers[pointerEvent.pointerId] = pointerEvent;
- }
+ switchConfigurations.apply(this);
- return Object.keys(this.pointers).length;
- },
+ radioButton1.onchange = switchConfigurations.bind(this);
+ radioButton2.onchange = switchConfigurations.bind(this);
+ radioButton3.onchange = switchConfigurations.bind(this);
+ }
+ };
- /**
- * check if ev matches pointertype
- * @param {String} pointerType Hammer.POINTER_MOUSE
- * @param {PointerEvent} ev
- */
- matchType: function(pointerType, ev) {
- if(!ev.pointerType) {
- return false;
- }
+ /**
+ * This overwrites the this.constants.
+ *
+ * @param constantsVariableName
+ * @param value
+ * @private
+ */
+ exports._overWriteGraphConstants = function (constantsVariableName, value) {
+ var nameArray = constantsVariableName.split("_");
+ if (nameArray.length == 1) {
+ this.constants[nameArray[0]] = value;
+ }
+ else if (nameArray.length == 2) {
+ this.constants[nameArray[0]][nameArray[1]] = value;
+ }
+ else if (nameArray.length == 3) {
+ this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
+ }
+ };
- var types = {};
- types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
- types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
- types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
- return types[pointerType];
- },
+ /**
+ * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
+ */
+ function graphToggleSmoothCurves () {
+ this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
+ else {graph_toggleSmooth.style.background = "#FF8532";}
- /**
- * get events
- */
- getEvents: function() {
- return [
- 'pointerdown MSPointerDown',
- 'pointermove MSPointerMove',
- 'pointerup pointercancel MSPointerUp MSPointerCancel'
- ];
- },
+ this._configureSmoothCurves(false);
+ }
- /**
- * reset the list
- */
- reset: function() {
- this.pointers = {};
+ /**
+ * this function is used to scramble the nodes
+ *
+ */
+ function graphRepositionNodes () {
+ for (var nodeId in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
+ this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
}
- };
-
+ }
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ }
+ else {
+ this.repositionNodes();
+ }
+ this.moving = true;
+ this.start();
+ }
- Hammer.utils = {
- /**
- * extend method,
- * also used for cloning when dest is an empty object
- * @param {Object} dest
- * @param {Object} src
- * @parm {Boolean} merge do a merge
- * @returns {Object} dest
- */
- extend: function extend(dest, src, merge) {
- for (var key in src) {
- if(dest[key] !== undefined && merge) {
- continue;
- }
- dest[key] = src[key];
+ /**
+ * this is used to generate an options file from the playing with physics system.
+ */
+ function graphGenerateOptions () {
+ var options = "No options are required, default values used.";
+ var optionsSpecific = [];
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ if (radioButton1.checked == true) {
+ if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
}
- return dest;
- },
-
-
- /**
- * find if a node is in the given parent
- * used for event delegation tricks
- * @param {HTMLElement} node
- * @param {HTMLElement} parent
- * @returns {boolean} has_parent
- */
- hasParent: function(node, parent) {
- while(node){
- if(node == parent) {
- return true;
- }
- node = node.parentNode;
+ }
+ options += '}}'
+ }
+ if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
+ if (optionsSpecific.length == 0) {options = "var options = {";}
+ else {options += ", "}
+ options += "smoothCurves: " + this.constants.smoothCurves.enabled;
+ }
+ if (options != "No options are required, default values used.") {
+ options += '};'
+ }
+ }
+ else if (radioButton2.checked == true) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {enabled: false}";
+ if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += ", repulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
}
- return false;
- },
-
+ }
+ options += '}}'
+ }
+ if (optionsSpecific.length == 0) {options += "}"}
+ if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
+ options += ", smoothCurves: " + this.constants.smoothCurves;
+ }
+ options += '};'
+ }
+ else {
+ options = "var options = {";
+ if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += "physics: {hierarchicalRepulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", ";
+ }
+ }
+ options += '}},';
+ }
+ options += 'hierarchicalLayout: {';
+ optionsSpecific = [];
+ if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
+ if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
+ if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
+ if (optionsSpecific.length != 0) {
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}'
+ }
+ else {
+ options += "enabled:true}";
+ }
+ options += '};'
+ }
- /**
- * get the center of all the touches
- * @param {Array} touches
- * @returns {Object} center
- */
- getCenter: function getCenter(touches) {
- var valuesX = [], valuesY = [];
- for(var t= 0,len=touches.length; t= y) {
- return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
- }
- else {
- return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
- }
- },
+ /*! Hammer.JS - v1.0.5 - 2013-04-07
+ * http://eightmedia.github.com/hammer.js
+ *
+ * Copyright (c) 2013 Jorik Tangelder ;
+ * Licensed under the MIT license */
+ (function(window, undefined) {
+ 'use strict';
- /**
- * calculate the distance between two touches
- * @param {Touch} touch1
- * @param {Touch} touch2
- * @returns {Number} distance
- */
- getDistance: function getDistance(touch1, touch2) {
- var x = touch2.pageX - touch1.pageX,
- y = touch2.pageY - touch1.pageY;
- return Math.sqrt((x*x) + (y*y));
- },
+ /**
+ * Hammer
+ * use this to create instances
+ * @param {HTMLElement} element
+ * @param {Object} options
+ * @returns {Hammer.Instance}
+ * @constructor
+ */
+ var Hammer = function(element, options) {
+ return new Hammer.Instance(element, options || {});
+ };
+ // default settings
+ Hammer.defaults = {
+ // add styles and attributes to the element to prevent the browser from doing
+ // its native behavior. this doesnt prevent the scrolling, but cancels
+ // the contextmenu, tap highlighting etc
+ // set to false to disable this
+ stop_browser_behavior: {
+ // this also triggers onselectstart=false for IE
+ userSelect: 'none',
+ // this makes the element blocking in IE10 >, you could experiment with the value
+ // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
+ touchAction: 'none',
+ touchCallout: 'none',
+ contentZooming: 'none',
+ userDrag: 'none',
+ tapHighlightColor: 'rgba(0,0,0,0)'
+ }
- /**
- * calculate the scale factor between two touchLists (fingers)
- * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
- * @param {Array} start
- * @param {Array} end
- * @returns {Number} scale
- */
- getScale: function getScale(start, end) {
- // need two fingers...
- if(start.length >= 2 && end.length >= 2) {
- return this.getDistance(end[0], end[1]) /
- this.getDistance(start[0], start[1]);
- }
- return 1;
- },
+ // more settings are defined per gesture at gestures.js
+ };
+ // detect touchevents
+ Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
+ Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
- /**
- * calculate the rotation degrees between two touchLists (fingers)
- * @param {Array} start
- * @param {Array} end
- * @returns {Number} rotation
- */
- getRotation: function getRotation(start, end) {
- // need two fingers
- if(start.length >= 2 && end.length >= 2) {
- return this.getAngle(end[1], end[0]) -
- this.getAngle(start[1], start[0]);
- }
- return 0;
- },
+ // dont use mouseevents on mobile devices
+ Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
+ Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
+ // eventtypes per touchevent (start, move, end)
+ // are filled by Hammer.event.determineEventTypes on setup
+ Hammer.EVENT_TYPES = {};
- /**
- * boolean if the direction is vertical
- * @param {String} direction
- * @returns {Boolean} is_vertical
- */
- isVertical: function isVertical(direction) {
- return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
- },
+ // direction defines
+ Hammer.DIRECTION_DOWN = 'down';
+ Hammer.DIRECTION_LEFT = 'left';
+ Hammer.DIRECTION_UP = 'up';
+ Hammer.DIRECTION_RIGHT = 'right';
+ // pointer type
+ Hammer.POINTER_MOUSE = 'mouse';
+ Hammer.POINTER_TOUCH = 'touch';
+ Hammer.POINTER_PEN = 'pen';
- /**
- * stop browser default behavior with css props
- * @param {HtmlElement} element
- * @param {Object} css_props
- */
- stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
- var prop,
- vendors = ['webkit','khtml','moz','ms','o',''];
+ // touch event defines
+ Hammer.EVENT_START = 'start';
+ Hammer.EVENT_MOVE = 'move';
+ Hammer.EVENT_END = 'end';
- if(!css_props || !element.style) {
- return;
- }
+ // hammer document where the base events are added at
+ Hammer.DOCUMENT = document;
- // with css properties for modern browsers
- for(var i = 0; i < vendors.length; i++) {
- for(var p in css_props) {
- if(css_props.hasOwnProperty(p)) {
- prop = p;
+ // plugins namespace
+ Hammer.plugins = {};
- // vender prefix at the property
- if(vendors[i]) {
- prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
- }
+ // if the window events are set...
+ Hammer.READY = false;
- // set the style
- element.style[prop] = css_props[p];
- }
- }
- }
+ /**
+ * setup events to detect gestures on the document
+ */
+ function setup() {
+ if(Hammer.READY) {
+ return;
+ }
- // also the disable onselectstart
- if(css_props.userSelect == 'none') {
- element.onselectstart = function() {
- return false;
- };
+ // find what eventtypes we add listeners to
+ Hammer.event.determineEventTypes();
+
+ // Register all gestures inside Hammer.gestures
+ for(var name in Hammer.gestures) {
+ if(Hammer.gestures.hasOwnProperty(name)) {
+ Hammer.detection.register(Hammer.gestures[name]);
}
}
- };
- Hammer.detection = {
- // contains all registred Hammer.gestures in the correct order
- gestures: [],
+ // Add touch events on the document
+ Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
+ Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
- // data of the current Hammer.gesture detection session
- current: null,
+ // Hammer is ready...!
+ Hammer.READY = true;
+ }
- // the previous Hammer.gesture session data
- // is a full clone of the previous gesture.current object
- previous: null,
+ /**
+ * create new hammer instance
+ * all methods should return the instance itself, so it is chainable.
+ * @param {HTMLElement} element
+ * @param {Object} [options={}]
+ * @returns {Hammer.Instance}
+ * @constructor
+ */
+ Hammer.Instance = function(element, options) {
+ var self = this;
- // when this becomes true, no gestures are fired
- stopped: false,
+ // setup HammerJS window events and register all gestures
+ // this also sets up the default options
+ setup();
+ this.element = element;
- /**
- * start Hammer.gesture detection
- * @param {Hammer.Instance} inst
- * @param {Object} eventData
- */
- startDetect: function startDetect(inst, eventData) {
- // already busy with a Hammer.gesture detection on an element
- if(this.current) {
- return;
- }
+ // start/stop detection option
+ this.enabled = true;
- this.stopped = false;
+ // merge options
+ this.options = Hammer.utils.extend(
+ Hammer.utils.extend({}, Hammer.defaults),
+ options || {});
- this.current = {
- inst : inst, // reference to HammerInstance we're working for
- startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
- lastEvent : false, // last eventData
- name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
- };
+ // add some css to the element to prevent the browser from doing its native behavoir
+ if(this.options.stop_browser_behavior) {
+ Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
+ }
- this.detect(eventData);
- },
+ // start detection on touchstart
+ Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
+ if(self.enabled) {
+ Hammer.detection.startDetect(self, ev);
+ }
+ });
+
+ // return instance
+ return this;
+ };
+ Hammer.Instance.prototype = {
/**
- * Hammer.gesture detection
- * @param {Object} eventData
- * @param {Object} eventData
+ * bind events to the instance
+ * @param {String} gesture
+ * @param {Function} handler
+ * @returns {Hammer.Instance}
*/
- detect: function detect(eventData) {
- if(!this.current || this.stopped) {
- return;
+ on: function onEvent(gesture, handler){
+ var gestures = gesture.split(' ');
+ for(var t=0; t 0 && eventType == Hammer.EVENT_END) {
+ eventType = Hammer.EVENT_MOVE;
+ }
+ // no touches, force the end event
+ else if(!count_touches) {
+ eventType = Hammer.EVENT_END;
+ }
- // set its index
- gesture.index = gesture.index || 1000;
+ // because touchend has no touches, and we often want to use these in our gestures,
+ // we send the last move event as our eventData in touchend
+ if(!count_touches && last_move_event !== null) {
+ ev = last_move_event;
+ }
+ // store the last move event
+ else {
+ last_move_event = ev;
+ }
- // add Hammer.gesture to the list
- this.gestures.push(gesture);
+ // trigger the handler
+ handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
- // sort the list by index
- this.gestures.sort(function(a, b) {
- if (a.index < b.index) {
- return -1;
+ // remove pointerevent from list
+ if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
+ count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
+ }
}
- if (a.index > b.index) {
- return 1;
+
+ //debug(sourceEventType +" "+ eventType);
+
+ // on the end we reset everything
+ if(!count_touches) {
+ last_move_event = null;
+ enable_detect = false;
+ touch_triggered = false;
+ Hammer.PointerEvent.reset();
}
- return 0;
});
+ },
- return this.gestures;
- }
- };
+ /**
+ * we have different events for each device/browser
+ * determine what we need and set them in the Hammer.EVENT_TYPES constant
+ */
+ determineEventTypes: function determineEventTypes() {
+ // determine the eventtype we want to set
+ var types;
- Hammer.gestures = Hammer.gestures || {};
+ // pointerEvents magic
+ if(Hammer.HAS_POINTEREVENTS) {
+ types = Hammer.PointerEvent.getEvents();
+ }
+ // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
+ else if(Hammer.NO_MOUSEEVENTS) {
+ types = [
+ 'touchstart',
+ 'touchmove',
+ 'touchend touchcancel'];
+ }
+ // for non pointer events browsers and mixed browsers,
+ // like chrome on windows8 touch laptop
+ else {
+ types = [
+ 'touchstart mousedown',
+ 'touchmove mousemove',
+ 'touchend touchcancel mouseup'];
+ }
- /**
- * Custom gestures
- * ==============================
- *
- * Gesture object
- * --------------------
- * The object structure of a gesture:
- *
- * { name: 'mygesture',
- * index: 1337,
- * defaults: {
- * mygesture_option: true
- * }
- * handler: function(type, ev, inst) {
- * // trigger gesture event
- * inst.trigger(this.name, ev);
- * }
- * }
+ Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
+ Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
+ Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
+ },
- * @param {String} name
- * this should be the name of the gesture, lowercase
- * it is also being used to disable/enable the gesture per instance config.
- *
- * @param {Number} [index=1000]
- * the index of the gesture, where it is going to be in the stack of gestures detection
- * like when you build an gesture that depends on the drag gesture, it is a good
- * idea to place it after the index of the drag gesture.
- *
- * @param {Object} [defaults={}]
- * the default settings of the gesture. these are added to the instance settings,
- * and can be overruled per instance. you can also add the name of the gesture,
- * but this is also added by default (and set to true).
- *
- * @param {Function} handler
- * this handles the gesture detection of your custom gesture and receives the
- * following arguments:
- *
- * @param {Object} eventData
- * event data containing the following properties:
- * timeStamp {Number} time the event occurred
- * target {HTMLElement} target element
- * touches {Array} touches (fingers, pointers, mouse) on the screen
- * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
- * center {Object} center position of the touches. contains pageX and pageY
- * deltaTime {Number} the total time of the touches in the screen
- * deltaX {Number} the delta on x axis we haved moved
- * deltaY {Number} the delta on y axis we haved moved
- * velocityX {Number} the velocity on the x
- * velocityY {Number} the velocity on y
- * angle {Number} the angle we are moving
- * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
- * distance {Number} the distance we haved moved
- * scale {Number} scaling of the touches, needs 2 touches
- * rotation {Number} rotation of the touches, needs 2 touches *
- * eventType {String} matches Hammer.EVENT_START|MOVE|END
- * srcEvent {Object} the source event, like TouchStart or MouseDown *
- * startEvent {Object} contains the same properties as above,
- * but from the first touch. this is used to calculate
- * distances, deltaTime, scaling etc
- *
- * @param {Hammer.Instance} inst
- * the instance we are doing the detection for. you can get the options from
- * the inst.options object and trigger the gesture event by calling inst.trigger
- *
- *
- * Handle gestures
- * --------------------
- * inside the handler you can get/set Hammer.detection.current. This is the current
- * detection session. It has the following properties
- * @param {String} name
- * contains the name of the gesture we have detected. it has not a real function,
- * only to check in other gestures if something is detected.
- * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
- * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
- *
- * @readonly
- * @param {Hammer.Instance} inst
- * the instance we do the detection for
- *
- * @readonly
- * @param {Object} startEvent
- * contains the properties of the first gesture detection in this session.
- * Used for calculations about timing, distance, etc.
- *
- * @readonly
- * @param {Object} lastEvent
- * contains all the properties of the last gesture detect in this session.
- *
- * after the gesture detection session has been completed (user has released the screen)
- * the Hammer.detection.current object is copied into Hammer.detection.previous,
- * this is usefull for gestures like doubletap, where you need to know if the
- * previous gesture was a tap
- *
- * options that have been set by the instance can be received by calling inst.options
- *
- * You can trigger a gesture event by calling inst.trigger("mygesture", event).
- * The first param is the name of your gesture, the second the event argument
- *
- *
- * Register gestures
- * --------------------
- * When an gesture is added to the Hammer.gestures object, it is auto registered
- * at the setup of the first Hammer instance. You can also call Hammer.detection.register
- * manually and pass your gesture object as a param
- *
- */
- /**
- * Hold
- * Touch stays at the same place for x time
- * @events hold
- */
- Hammer.gestures.Hold = {
- name: 'hold',
- index: 10,
- defaults: {
- hold_timeout : 500,
- hold_threshold : 1
+ /**
+ * create touchlist depending on the event
+ * @param {Object} ev
+ * @param {String} eventType used by the fakemultitouch plugin
+ */
+ getTouchList: function getTouchList(ev/*, eventType*/) {
+ // get the fake pointerEvent touchlist
+ if(Hammer.HAS_POINTEREVENTS) {
+ return Hammer.PointerEvent.getTouchList();
+ }
+ // get the touchlist
+ else if(ev.touches) {
+ return ev.touches;
+ }
+ // make fake touchlist from mouse position
+ else {
+ return [{
+ identifier: 1,
+ pageX: ev.pageX,
+ pageY: ev.pageY,
+ target: ev.target
+ }];
+ }
},
- timer: null,
- handler: function holdGesture(ev, inst) {
- switch(ev.eventType) {
- case Hammer.EVENT_START:
- // clear any running timers
- clearTimeout(this.timer);
- // set the gesture so we can check in the timeout if it still is
- Hammer.detection.current.name = this.name;
- // set timer and if after the timeout it still is hold,
- // we trigger the hold event
- this.timer = setTimeout(function() {
- if(Hammer.detection.current.name == 'hold') {
- inst.trigger('hold', ev);
- }
- }, inst.options.hold_timeout);
- break;
+ /**
+ * collect event data for Hammer js
+ * @param {HTMLElement} element
+ * @param {String} eventType like Hammer.EVENT_MOVE
+ * @param {Object} eventData
+ */
+ collectEventData: function collectEventData(element, eventType, ev) {
+ var touches = this.getTouchList(ev, eventType);
- // when you move or end we clear the timer
- case Hammer.EVENT_MOVE:
- if(ev.distance > inst.options.hold_threshold) {
- clearTimeout(this.timer);
+ // find out pointerType
+ var pointerType = Hammer.POINTER_TOUCH;
+ if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
+ pointerType = Hammer.POINTER_MOUSE;
+ }
+
+ return {
+ center : Hammer.utils.getCenter(touches),
+ timeStamp : new Date().getTime(),
+ target : ev.target,
+ touches : touches,
+ eventType : eventType,
+ pointerType : pointerType,
+ srcEvent : ev,
+
+ /**
+ * prevent the browser default actions
+ * mostly used to disable scrolling of the browser
+ */
+ preventDefault: function() {
+ if(this.srcEvent.preventManipulation) {
+ this.srcEvent.preventManipulation();
}
- break;
- case Hammer.EVENT_END:
- clearTimeout(this.timer);
- break;
- }
+ if(this.srcEvent.preventDefault) {
+ this.srcEvent.preventDefault();
+ }
+ },
+
+ /**
+ * stop bubbling the event up to its parents
+ */
+ stopPropagation: function() {
+ this.srcEvent.stopPropagation();
+ },
+
+ /**
+ * immediately stop gesture detection
+ * might be useful after a swipe was detected
+ * @return {*}
+ */
+ stopDetect: function() {
+ return Hammer.detection.stopDetect();
+ }
+ };
}
};
+ Hammer.PointerEvent = {
+ /**
+ * holds all pointers
+ * @type {Object}
+ */
+ pointers: {},
- /**
- * Tap/DoubleTap
- * Quick touch at a place or double at the same place
- * @events tap, doubletap
- */
- Hammer.gestures.Tap = {
- name: 'tap',
- index: 100,
- defaults: {
- tap_max_touchtime : 250,
- tap_max_distance : 10,
- tap_always : true,
- doubletap_distance : 20,
- doubletap_interval : 300
+ /**
+ * get a list of pointers
+ * @returns {Array} touchlist
+ */
+ getTouchList: function() {
+ var self = this;
+ var touchlist = [];
+
+ // we can use forEach since pointerEvents only is in IE10
+ Object.keys(self.pointers).sort().forEach(function(id) {
+ touchlist.push(self.pointers[id]);
+ });
+ return touchlist;
},
- handler: function tapGesture(ev, inst) {
- if(ev.eventType == Hammer.EVENT_END) {
- // previous gesture, for the double tap since these are two different gesture detections
- var prev = Hammer.detection.previous,
- did_doubletap = false;
- // when the touchtime is higher then the max touch time
- // or when the moving distance is too much
- if(ev.deltaTime > inst.options.tap_max_touchtime ||
- ev.distance > inst.options.tap_max_distance) {
- return;
- }
+ /**
+ * update the position of a pointer
+ * @param {String} type Hammer.EVENT_END
+ * @param {Object} pointerEvent
+ */
+ updatePointer: function(type, pointerEvent) {
+ if(type == Hammer.EVENT_END) {
+ this.pointers = {};
+ }
+ else {
+ pointerEvent.identifier = pointerEvent.pointerId;
+ this.pointers[pointerEvent.pointerId] = pointerEvent;
+ }
- // check if double tap
- if(prev && prev.name == 'tap' &&
- (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
- ev.distance < inst.options.doubletap_distance) {
- inst.trigger('doubletap', ev);
- did_doubletap = true;
- }
+ return Object.keys(this.pointers).length;
+ },
- // do a single tap
- if(!did_doubletap || inst.options.tap_always) {
- Hammer.detection.current.name = 'tap';
- inst.trigger(Hammer.detection.current.name, ev);
- }
+ /**
+ * check if ev matches pointertype
+ * @param {String} pointerType Hammer.POINTER_MOUSE
+ * @param {PointerEvent} ev
+ */
+ matchType: function(pointerType, ev) {
+ if(!ev.pointerType) {
+ return false;
}
+
+ var types = {};
+ types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
+ types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
+ types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
+ return types[pointerType];
+ },
+
+
+ /**
+ * get events
+ */
+ getEvents: function() {
+ return [
+ 'pointerdown MSPointerDown',
+ 'pointermove MSPointerMove',
+ 'pointerup pointercancel MSPointerUp MSPointerCancel'
+ ];
+ },
+
+ /**
+ * reset the list
+ */
+ reset: function() {
+ this.pointers = {};
}
};
- /**
- * Swipe
- * triggers swipe events when the end velocity is above the threshold
- * @events swipe, swipeleft, swiperight, swipeup, swipedown
- */
- Hammer.gestures.Swipe = {
- name: 'swipe',
- index: 40,
- defaults: {
- // set 0 for unlimited, but this can conflict with transform
- swipe_max_touches : 1,
- swipe_velocity : 0.7
+ Hammer.utils = {
+ /**
+ * extend method,
+ * also used for cloning when dest is an empty object
+ * @param {Object} dest
+ * @param {Object} src
+ * @parm {Boolean} merge do a merge
+ * @returns {Object} dest
+ */
+ extend: function extend(dest, src, merge) {
+ for (var key in src) {
+ if(dest[key] !== undefined && merge) {
+ continue;
+ }
+ dest[key] = src[key];
+ }
+ return dest;
},
- handler: function swipeGesture(ev, inst) {
- if(ev.eventType == Hammer.EVENT_END) {
- // max touches
- if(inst.options.swipe_max_touches > 0 &&
- ev.touches.length > inst.options.swipe_max_touches) {
- return;
- }
- // when the distance we moved is too small we skip this gesture
- // or we can be already in dragging
- if(ev.velocityX > inst.options.swipe_velocity ||
- ev.velocityY > inst.options.swipe_velocity) {
- // trigger swipe events
- inst.trigger(this.name, ev);
- inst.trigger(this.name + ev.direction, ev);
+
+ /**
+ * find if a node is in the given parent
+ * used for event delegation tricks
+ * @param {HTMLElement} node
+ * @param {HTMLElement} parent
+ * @returns {boolean} has_parent
+ */
+ hasParent: function(node, parent) {
+ while(node){
+ if(node == parent) {
+ return true;
}
+ node = node.parentNode;
}
- }
- };
+ return false;
+ },
- /**
- * Drag
- * Move with x fingers (default 1) around on the page. Blocking the scrolling when
- * moving left and right is a good practice. When all the drag events are blocking
- * you disable scrolling on that area.
- * @events drag, drapleft, dragright, dragup, dragdown
- */
- Hammer.gestures.Drag = {
- name: 'drag',
- index: 50,
- defaults: {
- drag_min_distance : 10,
- // set 0 for unlimited, but this can conflict with transform
- drag_max_touches : 1,
- // prevent default browser behavior when dragging occurs
- // be careful with it, it makes the element a blocking element
- // when you are using the drag gesture, it is a good practice to set this true
- drag_block_horizontal : false,
- drag_block_vertical : false,
- // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
- // It disallows vertical directions if the initial direction was horizontal, and vice versa.
- drag_lock_to_axis : false,
- // drag lock only kicks in when distance > drag_lock_min_distance
- // This way, locking occurs only when the distance has become large enough to reliably determine the direction
- drag_lock_min_distance : 25
- },
- triggered: false,
- handler: function dragGesture(ev, inst) {
- // current gesture isnt drag, but dragged is true
- // this means an other gesture is busy. now call dragend
- if(Hammer.detection.current.name != this.name && this.triggered) {
- inst.trigger(this.name +'end', ev);
- this.triggered = false;
- return;
- }
+ /**
+ * get the center of all the touches
+ * @param {Array} touches
+ * @returns {Object} center
+ */
+ getCenter: function getCenter(touches) {
+ var valuesX = [], valuesY = [];
- // max touches
- if(inst.options.drag_max_touches > 0 &&
- ev.touches.length > inst.options.drag_max_touches) {
- return;
+ for(var t= 0,len=touches.length; t= y) {
+ return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
+ }
+ else {
+ return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
+ }
+ },
- case Hammer.EVENT_END:
- // trigger dragend
- if(this.triggered) {
- inst.trigger(this.name +'end', ev);
- }
- this.triggered = false;
- break;
+ /**
+ * calculate the distance between two touches
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @returns {Number} distance
+ */
+ getDistance: function getDistance(touch1, touch2) {
+ var x = touch2.pageX - touch1.pageX,
+ y = touch2.pageY - touch1.pageY;
+ return Math.sqrt((x*x) + (y*y));
+ },
+
+
+ /**
+ * calculate the scale factor between two touchLists (fingers)
+ * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
+ * @param {Array} start
+ * @param {Array} end
+ * @returns {Number} scale
+ */
+ getScale: function getScale(start, end) {
+ // need two fingers...
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getDistance(end[0], end[1]) /
+ this.getDistance(start[0], start[1]);
}
- }
- };
+ return 1;
+ },
- /**
- * Transform
- * User want to scale or rotate with 2 fingers
- * @events transform, pinch, pinchin, pinchout, rotate
- */
- Hammer.gestures.Transform = {
- name: 'transform',
- index: 45,
- defaults: {
- // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
- transform_min_scale : 0.01,
- // rotation in degrees
- transform_min_rotation : 1,
- // prevent default browser behavior when two touches are on the screen
- // but it makes the element a blocking element
- // when you are using the transform gesture, it is a good practice to set this true
- transform_always_block : false
+ /**
+ * calculate the rotation degrees between two touchLists (fingers)
+ * @param {Array} start
+ * @param {Array} end
+ * @returns {Number} rotation
+ */
+ getRotation: function getRotation(start, end) {
+ // need two fingers
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getAngle(end[1], end[0]) -
+ this.getAngle(start[1], start[0]);
+ }
+ return 0;
},
- triggered: false,
- handler: function transformGesture(ev, inst) {
- // current gesture isnt drag, but dragged is true
- // this means an other gesture is busy. now call dragend
- if(Hammer.detection.current.name != this.name && this.triggered) {
- inst.trigger(this.name +'end', ev);
- this.triggered = false;
+
+
+ /**
+ * boolean if the direction is vertical
+ * @param {String} direction
+ * @returns {Boolean} is_vertical
+ */
+ isVertical: function isVertical(direction) {
+ return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
+ },
+
+
+ /**
+ * stop browser default behavior with css props
+ * @param {HtmlElement} element
+ * @param {Object} css_props
+ */
+ stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
+ var prop,
+ vendors = ['webkit','khtml','moz','ms','o',''];
+
+ if(!css_props || !element.style) {
return;
}
- // atleast multitouch
- if(ev.touches.length < 2) {
- return;
+ // with css properties for modern browsers
+ for(var i = 0; i < vendors.length; i++) {
+ for(var p in css_props) {
+ if(css_props.hasOwnProperty(p)) {
+ prop = p;
+
+ // vender prefix at the property
+ if(vendors[i]) {
+ prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
+ }
+
+ // set the style
+ element.style[prop] = css_props[p];
+ }
+ }
}
- // prevent default when two fingers are on the screen
- if(inst.options.transform_always_block) {
- ev.preventDefault();
+ // also the disable onselectstart
+ if(css_props.userSelect == 'none') {
+ element.onselectstart = function() {
+ return false;
+ };
}
+ }
+ };
- switch(ev.eventType) {
- case Hammer.EVENT_START:
- this.triggered = false;
- break;
+ Hammer.detection = {
+ // contains all registred Hammer.gestures in the correct order
+ gestures: [],
- case Hammer.EVENT_MOVE:
- var scale_threshold = Math.abs(1-ev.scale);
- var rotation_threshold = Math.abs(ev.rotation);
+ // data of the current Hammer.gesture detection session
+ current: null,
- // when the distance we moved is too small we skip this gesture
- // or we can be already in dragging
- if(scale_threshold < inst.options.transform_min_scale &&
- rotation_threshold < inst.options.transform_min_rotation) {
- return;
- }
+ // the previous Hammer.gesture session data
+ // is a full clone of the previous gesture.current object
+ previous: null,
- // we are transforming!
- Hammer.detection.current.name = this.name;
+ // when this becomes true, no gestures are fired
+ stopped: false,
- // first time, trigger dragstart event
- if(!this.triggered) {
- inst.trigger(this.name +'start', ev);
- this.triggered = true;
- }
- inst.trigger(this.name, ev); // basic transform event
+ /**
+ * start Hammer.gesture detection
+ * @param {Hammer.Instance} inst
+ * @param {Object} eventData
+ */
+ startDetect: function startDetect(inst, eventData) {
+ // already busy with a Hammer.gesture detection on an element
+ if(this.current) {
+ return;
+ }
- // trigger rotate event
- if(rotation_threshold > inst.options.transform_min_rotation) {
- inst.trigger('rotate', ev);
- }
+ this.stopped = false;
- // trigger pinch event
- if(scale_threshold > inst.options.transform_min_scale) {
- inst.trigger('pinch', ev);
- inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
- }
- break;
+ this.current = {
+ inst : inst, // reference to HammerInstance we're working for
+ startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
+ lastEvent : false, // last eventData
+ name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
+ };
- case Hammer.EVENT_END:
- // trigger dragend
- if(this.triggered) {
- inst.trigger(this.name +'end', ev);
- }
+ this.detect(eventData);
+ },
- this.triggered = false;
- break;
+
+ /**
+ * Hammer.gesture detection
+ * @param {Object} eventData
+ * @param {Object} eventData
+ */
+ detect: function detect(eventData) {
+ if(!this.current || this.stopped) {
+ return;
}
- }
- };
+ // extend event data with calculations about scale, distance etc
+ eventData = this.extendEventData(eventData);
- /**
- * Touch
- * Called as first, tells the user has touched the screen
- * @events touch
- */
- Hammer.gestures.Touch = {
- name: 'touch',
- index: -Infinity,
- defaults: {
- // call preventDefault at touchstart, and makes the element blocking by
- // disabling the scrolling of the page, but it improves gestures like
- // transforming and dragging.
- // be careful with using this, it can be very annoying for users to be stuck
- // on the page
- prevent_default: false,
+ // instance options
+ var inst_options = this.current.inst.options;
- // disable mouse events, so only touch (or pen!) input triggers events
- prevent_mouseevents: false
- },
- handler: function touchGesture(ev, inst) {
- if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
- ev.stopDetect();
- return;
+ // call Hammer.gesture handlers
+ for(var g=0,len=this.gestures.length; g this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
- this.clusterToFit(this.constants.clustering.reduceToNodes, false);
- }
- // we now start the force calculation
- this._calculateForces();
- }
- };
+ /**
+ * register new gesture
+ * @param {Object} gesture object, see gestures.js for documentation
+ * @returns {Array} gestures
+ */
+ register: function register(gesture) {
+ // add an enable gesture options if there is no given
+ var options = gesture.defaults || {};
+ if(options[gesture.name] === undefined) {
+ options[gesture.name] = true;
+ }
+ // extend Hammer default options with the Hammer.gesture options
+ Hammer.utils.extend(Hammer.defaults, options, true);
- /**
- * Calculate the external forces acting on the nodes
- * Forces are caused by: edges, repulsing forces between nodes, gravity
- * @private
- */
- exports._calculateForces = function () {
- // Gravity is required to keep separated groups from floating off
- // the forces are reset to zero in this loop by using _setForce instead
- // of _addForce
+ // set its index
+ gesture.index = gesture.index || 1000;
- this._calculateGravitationalForces();
- this._calculateNodeForces();
+ // add Hammer.gesture to the list
+ this.gestures.push(gesture);
- if (this.constants.smoothCurves == true) {
- this._calculateSpringForcesWithSupport();
- }
- else {
- if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
- this._calculateHierarchicalSpringForces();
- }
- else {
- this._calculateSpringForces();
+ // sort the list by index
+ this.gestures.sort(function(a, b) {
+ if (a.index < b.index) {
+ return -1;
+ }
+ if (a.index > b.index) {
+ return 1;
+ }
+ return 0;
+ });
+
+ return this.gestures;
}
- }
};
+ Hammer.gestures = Hammer.gestures || {};
+
/**
- * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
- * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
- * This function joins the datanodes and invisible (called support) nodes into one object.
- * We do this so we do not contaminate this.nodes with the support nodes.
+ * Custom gestures
+ * ==============================
+ *
+ * Gesture object
+ * --------------------
+ * The object structure of a gesture:
+ *
+ * { name: 'mygesture',
+ * index: 1337,
+ * defaults: {
+ * mygesture_option: true
+ * }
+ * handler: function(type, ev, inst) {
+ * // trigger gesture event
+ * inst.trigger(this.name, ev);
+ * }
+ * }
+
+ * @param {String} name
+ * this should be the name of the gesture, lowercase
+ * it is also being used to disable/enable the gesture per instance config.
+ *
+ * @param {Number} [index=1000]
+ * the index of the gesture, where it is going to be in the stack of gestures detection
+ * like when you build an gesture that depends on the drag gesture, it is a good
+ * idea to place it after the index of the drag gesture.
+ *
+ * @param {Object} [defaults={}]
+ * the default settings of the gesture. these are added to the instance settings,
+ * and can be overruled per instance. you can also add the name of the gesture,
+ * but this is also added by default (and set to true).
+ *
+ * @param {Function} handler
+ * this handles the gesture detection of your custom gesture and receives the
+ * following arguments:
+ *
+ * @param {Object} eventData
+ * event data containing the following properties:
+ * timeStamp {Number} time the event occurred
+ * target {HTMLElement} target element
+ * touches {Array} touches (fingers, pointers, mouse) on the screen
+ * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
+ * center {Object} center position of the touches. contains pageX and pageY
+ * deltaTime {Number} the total time of the touches in the screen
+ * deltaX {Number} the delta on x axis we haved moved
+ * deltaY {Number} the delta on y axis we haved moved
+ * velocityX {Number} the velocity on the x
+ * velocityY {Number} the velocity on y
+ * angle {Number} the angle we are moving
+ * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
+ * distance {Number} the distance we haved moved
+ * scale {Number} scaling of the touches, needs 2 touches
+ * rotation {Number} rotation of the touches, needs 2 touches *
+ * eventType {String} matches Hammer.EVENT_START|MOVE|END
+ * srcEvent {Object} the source event, like TouchStart or MouseDown *
+ * startEvent {Object} contains the same properties as above,
+ * but from the first touch. this is used to calculate
+ * distances, deltaTime, scaling etc
+ *
+ * @param {Hammer.Instance} inst
+ * the instance we are doing the detection for. you can get the options from
+ * the inst.options object and trigger the gesture event by calling inst.trigger
+ *
+ *
+ * Handle gestures
+ * --------------------
+ * inside the handler you can get/set Hammer.detection.current. This is the current
+ * detection session. It has the following properties
+ * @param {String} name
+ * contains the name of the gesture we have detected. it has not a real function,
+ * only to check in other gestures if something is detected.
+ * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
+ * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
+ *
+ * @readonly
+ * @param {Hammer.Instance} inst
+ * the instance we do the detection for
+ *
+ * @readonly
+ * @param {Object} startEvent
+ * contains the properties of the first gesture detection in this session.
+ * Used for calculations about timing, distance, etc.
+ *
+ * @readonly
+ * @param {Object} lastEvent
+ * contains all the properties of the last gesture detect in this session.
+ *
+ * after the gesture detection session has been completed (user has released the screen)
+ * the Hammer.detection.current object is copied into Hammer.detection.previous,
+ * this is usefull for gestures like doubletap, where you need to know if the
+ * previous gesture was a tap
+ *
+ * options that have been set by the instance can be received by calling inst.options
+ *
+ * You can trigger a gesture event by calling inst.trigger("mygesture", event).
+ * The first param is the name of your gesture, the second the event argument
+ *
+ *
+ * Register gestures
+ * --------------------
+ * When an gesture is added to the Hammer.gestures object, it is auto registered
+ * at the setup of the first Hammer instance. You can also call Hammer.detection.register
+ * manually and pass your gesture object as a param
*
- * @private
*/
- exports._updateCalculationNodes = function () {
- if (this.constants.smoothCurves == true) {
- this.calculationNodes = {};
- this.calculationNodeIndices = [];
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId] = this.nodes[nodeId];
- }
- }
- var supportNodes = this.sectors['support']['nodes'];
- for (var supportNodeId in supportNodes) {
- if (supportNodes.hasOwnProperty(supportNodeId)) {
- if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
- this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
- }
- else {
- supportNodes[supportNodeId]._setForce(0, 0);
- }
- }
- }
+ /**
+ * Hold
+ * Touch stays at the same place for x time
+ * @events hold
+ */
+ Hammer.gestures.Hold = {
+ name: 'hold',
+ index: 10,
+ defaults: {
+ hold_timeout : 500,
+ hold_threshold : 1
+ },
+ timer: null,
+ handler: function holdGesture(ev, inst) {
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ // clear any running timers
+ clearTimeout(this.timer);
- for (var idx in this.calculationNodes) {
- if (this.calculationNodes.hasOwnProperty(idx)) {
- this.calculationNodeIndices.push(idx);
- }
+ // set the gesture so we can check in the timeout if it still is
+ Hammer.detection.current.name = this.name;
+
+ // set timer and if after the timeout it still is hold,
+ // we trigger the hold event
+ this.timer = setTimeout(function() {
+ if(Hammer.detection.current.name == 'hold') {
+ inst.trigger('hold', ev);
+ }
+ }, inst.options.hold_timeout);
+ break;
+
+ // when you move or end we clear the timer
+ case Hammer.EVENT_MOVE:
+ if(ev.distance > inst.options.hold_threshold) {
+ clearTimeout(this.timer);
+ }
+ break;
+
+ case Hammer.EVENT_END:
+ clearTimeout(this.timer);
+ break;
+ }
}
- }
- else {
- this.calculationNodes = this.nodes;
- this.calculationNodeIndices = this.nodeIndices;
- }
};
/**
- * this function applies the central gravity effect to keep groups from floating off
- *
- * @private
+ * Tap/DoubleTap
+ * Quick touch at a place or double at the same place
+ * @events tap, doubletap
*/
- exports._calculateGravitationalForces = function () {
- var dx, dy, distance, node, i;
- var nodes = this.calculationNodes;
- var gravity = this.constants.physics.centralGravity;
- var gravityForce = 0;
+ Hammer.gestures.Tap = {
+ name: 'tap',
+ index: 100,
+ defaults: {
+ tap_max_touchtime : 250,
+ tap_max_distance : 10,
+ tap_always : true,
+ doubletap_distance : 20,
+ doubletap_interval : 300
+ },
+ handler: function tapGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ // previous gesture, for the double tap since these are two different gesture detections
+ var prev = Hammer.detection.previous,
+ did_doubletap = false;
- for (i = 0; i < this.calculationNodeIndices.length; i++) {
- node = nodes[this.calculationNodeIndices[i]];
- node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
- // gravity does not apply when we are in a pocket sector
- if (this._sector() == "default" && gravity != 0) {
- dx = -node.x;
- dy = -node.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ // when the touchtime is higher then the max touch time
+ // or when the moving distance is too much
+ if(ev.deltaTime > inst.options.tap_max_touchtime ||
+ ev.distance > inst.options.tap_max_distance) {
+ return;
+ }
- gravityForce = (distance == 0) ? 0 : (gravity / distance);
- node.fx = dx * gravityForce;
- node.fy = dy * gravityForce;
- }
- else {
- node.fx = 0;
- node.fy = 0;
+ // check if double tap
+ if(prev && prev.name == 'tap' &&
+ (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
+ ev.distance < inst.options.doubletap_distance) {
+ inst.trigger('doubletap', ev);
+ did_doubletap = true;
+ }
+
+ // do a single tap
+ if(!did_doubletap || inst.options.tap_always) {
+ Hammer.detection.current.name = 'tap';
+ inst.trigger(Hammer.detection.current.name, ev);
+ }
+ }
}
- }
};
-
-
/**
- * this function calculates the effects of the springs in the case of unsmooth curves.
- *
- * @private
+ * Swipe
+ * triggers swipe events when the end velocity is above the threshold
+ * @events swipe, swipeleft, swiperight, swipeup, swipedown
*/
- exports._calculateSpringForces = function () {
- var edgeLength, edge, edgeId;
- var dx, dy, fx, fy, springForce, distance;
- var edges = this.edges;
-
- // forces caused by the edges, modelled as springs
- for (edgeId in edges) {
- if (edges.hasOwnProperty(edgeId)) {
- edge = edges[edgeId];
- if (edge.connected) {
- // only calculate forces if nodes are in the same sector
- if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
- edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
- // this implies that the edges between big clusters are longer
- edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
-
- dx = (edge.from.x - edge.to.x);
- dy = (edge.from.y - edge.to.y);
- distance = Math.sqrt(dx * dx + dy * dy);
-
- if (distance == 0) {
- distance = 0.01;
- }
-
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
-
- fx = dx * springForce;
- fy = dy * springForce;
+ Hammer.gestures.Swipe = {
+ name: 'swipe',
+ index: 40,
+ defaults: {
+ // set 0 for unlimited, but this can conflict with transform
+ swipe_max_touches : 1,
+ swipe_velocity : 0.7
+ },
+ handler: function swipeGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ // max touches
+ if(inst.options.swipe_max_touches > 0 &&
+ ev.touches.length > inst.options.swipe_max_touches) {
+ return;
+ }
- edge.from.fx += fx;
- edge.from.fy += fy;
- edge.to.fx -= fx;
- edge.to.fy -= fy;
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.velocityX > inst.options.swipe_velocity ||
+ ev.velocityY > inst.options.swipe_velocity) {
+ // trigger swipe events
+ inst.trigger(this.name, ev);
+ inst.trigger(this.name + ev.direction, ev);
+ }
}
- }
}
- }
};
-
-
/**
- * This function calculates the springforces on the nodes, accounting for the support nodes.
- *
- * @private
+ * Drag
+ * Move with x fingers (default 1) around on the page. Blocking the scrolling when
+ * moving left and right is a good practice. When all the drag events are blocking
+ * you disable scrolling on that area.
+ * @events drag, drapleft, dragright, dragup, dragdown
*/
- exports._calculateSpringForcesWithSupport = function () {
- var edgeLength, edge, edgeId, combinedClusterSize;
- var edges = this.edges;
+ Hammer.gestures.Drag = {
+ name: 'drag',
+ index: 50,
+ defaults: {
+ drag_min_distance : 10,
+ // set 0 for unlimited, but this can conflict with transform
+ drag_max_touches : 1,
+ // prevent default browser behavior when dragging occurs
+ // be careful with it, it makes the element a blocking element
+ // when you are using the drag gesture, it is a good practice to set this true
+ drag_block_horizontal : false,
+ drag_block_vertical : false,
+ // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
+ // It disallows vertical directions if the initial direction was horizontal, and vice versa.
+ drag_lock_to_axis : false,
+ // drag lock only kicks in when distance > drag_lock_min_distance
+ // This way, locking occurs only when the distance has become large enough to reliably determine the direction
+ drag_lock_min_distance : 25
+ },
+ triggered: false,
+ handler: function dragGesture(ev, inst) {
+ // current gesture isnt drag, but dragged is true
+ // this means an other gesture is busy. now call dragend
+ if(Hammer.detection.current.name != this.name && this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ this.triggered = false;
+ return;
+ }
- // forces caused by the edges, modelled as springs
- for (edgeId in edges) {
- if (edges.hasOwnProperty(edgeId)) {
- edge = edges[edgeId];
- if (edge.connected) {
- // only calculate forces if nodes are in the same sector
- if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
- if (edge.via != null) {
- var node1 = edge.to;
- var node2 = edge.via;
- var node3 = edge.from;
+ // max touches
+ if(inst.options.drag_max_touches > 0 &&
+ ev.touches.length > inst.options.drag_max_touches) {
+ return;
+ }
- edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ this.triggered = false;
+ break;
- combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
+ case Hammer.EVENT_MOVE:
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.distance < inst.options.drag_min_distance &&
+ Hammer.detection.current.name != this.name) {
+ return;
+ }
- // this implies that the edges between big clusters are longer
- edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
- this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
- this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
- }
- }
- }
- }
- }
- };
+ // we are dragging!
+ Hammer.detection.current.name = this.name;
+ // lock drag to axis?
+ if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
+ ev.drag_locked_to_axis = true;
+ }
+ var last_direction = Hammer.detection.current.lastEvent.direction;
+ if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
+ // keep direction on the axis that the drag gesture started on
+ if(Hammer.utils.isVertical(last_direction)) {
+ ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
+ }
+ else {
+ ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
+ }
+ }
- /**
- * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
- *
- * @param node1
- * @param node2
- * @param edgeLength
- * @private
- */
- exports._calculateSpringForce = function (node1, node2, edgeLength) {
- var dx, dy, fx, fy, springForce, distance;
+ // first time, trigger dragstart event
+ if(!this.triggered) {
+ inst.trigger(this.name +'start', ev);
+ this.triggered = true;
+ }
- dx = (node1.x - node2.x);
- dy = (node1.y - node2.y);
- distance = Math.sqrt(dx * dx + dy * dy);
+ // trigger normal event
+ inst.trigger(this.name, ev);
- if (distance == 0) {
- distance = 0.01;
- }
+ // direction event, like dragdown
+ inst.trigger(this.name + ev.direction, ev);
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
+ // block the browser events
+ if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
+ (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
+ ev.preventDefault();
+ }
+ break;
- fx = dx * springForce;
- fy = dy * springForce;
+ case Hammer.EVENT_END:
+ // trigger dragend
+ if(this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ }
- node1.fx += fx;
- node1.fy += fy;
- node2.fx -= fx;
- node2.fy -= fy;
+ this.triggered = false;
+ break;
+ }
+ }
};
/**
- * Load the HTML for the physics config and bind it
- * @private
+ * Transform
+ * User want to scale or rotate with 2 fingers
+ * @events transform, pinch, pinchin, pinchout, rotate
*/
- exports._loadPhysicsConfiguration = function () {
- if (this.physicsConfiguration === undefined) {
- this.backupConstants = {};
- util.deepExtend(this.backupConstants,this.constants);
-
- var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
- this.physicsConfiguration = document.createElement('div');
- this.physicsConfiguration.className = "PhysicsConfiguration";
- this.physicsConfiguration.innerHTML = '' +
- '' +
- '' +
- '' +
- '' +
- ''
- this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
- this.optionsDiv = document.createElement("div");
- this.optionsDiv.style.fontSize = "14px";
- this.optionsDiv.style.fontFamily = "verdana";
- this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
-
- var rangeElement;
- rangeElement = document.getElementById('graph_BH_gc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
- rangeElement = document.getElementById('graph_BH_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_BH_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_BH_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_BH_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
-
- rangeElement = document.getElementById('graph_R_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
- rangeElement = document.getElementById('graph_R_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_R_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_R_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_R_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
+ Hammer.gestures.Transform = {
+ name: 'transform',
+ index: 45,
+ defaults: {
+ // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
+ transform_min_scale : 0.01,
+ // rotation in degrees
+ transform_min_rotation : 1,
+ // prevent default browser behavior when two touches are on the screen
+ // but it makes the element a blocking element
+ // when you are using the transform gesture, it is a good practice to set this true
+ transform_always_block : false
+ },
+ triggered: false,
+ handler: function transformGesture(ev, inst) {
+ // current gesture isnt drag, but dragged is true
+ // this means an other gesture is busy. now call dragend
+ if(Hammer.detection.current.name != this.name && this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ this.triggered = false;
+ return;
+ }
- rangeElement = document.getElementById('graph_H_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
- rangeElement = document.getElementById('graph_H_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_H_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_H_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_H_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
- rangeElement = document.getElementById('graph_H_direction');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
- rangeElement = document.getElementById('graph_H_levsep');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
- rangeElement = document.getElementById('graph_H_nspac');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
+ // atleast multitouch
+ if(ev.touches.length < 2) {
+ return;
+ }
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- var radioButton3 = document.getElementById("graph_physicsMethod3");
- radioButton2.checked = true;
- if (this.constants.physics.barnesHut.enabled) {
- radioButton1.checked = true;
- }
- if (this.constants.hierarchicalLayout.enabled) {
- radioButton3.checked = true;
- }
+ // prevent default when two fingers are on the screen
+ if(inst.options.transform_always_block) {
+ ev.preventDefault();
+ }
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- var graph_repositionNodes = document.getElementById("graph_repositionNodes");
- var graph_generateOptions = document.getElementById("graph_generateOptions");
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ this.triggered = false;
+ break;
- graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
- graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
- graph_generateOptions.onclick = graphGenerateOptions.bind(this);
- if (this.constants.smoothCurves == true) {
- graph_toggleSmooth.style.background = "#A4FF56";
- }
- else {
- graph_toggleSmooth.style.background = "#FF8532";
- }
+ case Hammer.EVENT_MOVE:
+ var scale_threshold = Math.abs(1-ev.scale);
+ var rotation_threshold = Math.abs(ev.rotation);
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(scale_threshold < inst.options.transform_min_scale &&
+ rotation_threshold < inst.options.transform_min_rotation) {
+ return;
+ }
- switchConfigurations.apply(this);
+ // we are transforming!
+ Hammer.detection.current.name = this.name;
- radioButton1.onchange = switchConfigurations.bind(this);
- radioButton2.onchange = switchConfigurations.bind(this);
- radioButton3.onchange = switchConfigurations.bind(this);
- }
- };
+ // first time, trigger dragstart event
+ if(!this.triggered) {
+ inst.trigger(this.name +'start', ev);
+ this.triggered = true;
+ }
- /**
- * This overwrites the this.constants.
- *
- * @param constantsVariableName
- * @param value
- * @private
- */
- exports._overWriteGraphConstants = function (constantsVariableName, value) {
- var nameArray = constantsVariableName.split("_");
- if (nameArray.length == 1) {
- this.constants[nameArray[0]] = value;
- }
- else if (nameArray.length == 2) {
- this.constants[nameArray[0]][nameArray[1]] = value;
- }
- else if (nameArray.length == 3) {
- this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
- }
- };
+ inst.trigger(this.name, ev); // basic transform event
+ // trigger rotate event
+ if(rotation_threshold > inst.options.transform_min_rotation) {
+ inst.trigger('rotate', ev);
+ }
- /**
- * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
- */
- function graphToggleSmoothCurves () {
- this.constants.smoothCurves = !this.constants.smoothCurves;
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
+ // trigger pinch event
+ if(scale_threshold > inst.options.transform_min_scale) {
+ inst.trigger('pinch', ev);
+ inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
+ }
+ break;
- this._configureSmoothCurves(false);
- }
+ case Hammer.EVENT_END:
+ // trigger dragend
+ if(this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ }
- /**
- * this function is used to scramble the nodes
- *
- */
- function graphRepositionNodes () {
- for (var nodeId in this.calculationNodes) {
- if (this.calculationNodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
- this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
+ this.triggered = false;
+ break;
+ }
}
- }
- if (this.constants.hierarchicalLayout.enabled == true) {
- this._setupHierarchicalLayout();
- }
- else {
- this.repositionNodes();
- }
- this.moving = true;
- this.start();
- }
+ };
+
/**
- * this is used to generate an options file from the playing with physics system.
+ * Touch
+ * Called as first, tells the user has touched the screen
+ * @events touch
*/
- function graphGenerateOptions () {
- var options = "No options are required, default values used.";
- var optionsSpecific = [];
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- if (radioButton1.checked == true) {
- if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options = "var options = {";
- options += "physics: {barnesHut: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}}'
- }
- if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
- if (optionsSpecific.length == 0) {options = "var options = {";}
- else {options += ", "}
- options += "smoothCurves: " + this.constants.smoothCurves;
- }
- if (options != "No options are required, default values used.") {
- options += '};'
- }
- }
- else if (radioButton2.checked == true) {
- options = "var options = {";
- options += "physics: {barnesHut: {enabled: false}";
- if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += ", repulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
+ Hammer.gestures.Touch = {
+ name: 'touch',
+ index: -Infinity,
+ defaults: {
+ // call preventDefault at touchstart, and makes the element blocking by
+ // disabling the scrolling of the page, but it improves gestures like
+ // transforming and dragging.
+ // be careful with using this, it can be very annoying for users to be stuck
+ // on the page
+ prevent_default: false,
+
+ // disable mouse events, so only touch (or pen!) input triggers events
+ prevent_mouseevents: false
+ },
+ handler: function touchGesture(ev, inst) {
+ if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
+ ev.stopDetect();
+ return;
}
- }
- options += '}}'
- }
- if (optionsSpecific.length == 0) {options += "}"}
- if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
- options += ", smoothCurves: " + this.constants.smoothCurves;
- }
- options += '};'
- }
- else {
- options = "var options = {";
- if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += "physics: {hierarchicalRepulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", ";
+
+ if(inst.options.prevent_default) {
+ ev.preventDefault();
}
- }
- options += '}},';
- }
- options += 'hierarchicalLayout: {';
- optionsSpecific = [];
- if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
- if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
- if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
- if (optionsSpecific.length != 0) {
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
+
+ if(ev.eventType == Hammer.EVENT_START) {
+ inst.trigger(this.name, ev);
}
- }
- options += '}'
}
- else {
- options += "enabled:true}";
- }
- options += '};'
- }
-
+ };
- this.optionsDiv.innerHTML = options;
- }
/**
- * this is used to switch between barnesHut, repulsion and hierarchical.
- *
+ * Release
+ * Called as last, tells the user has released the screen
+ * @events release
*/
- function switchConfigurations () {
- var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
- var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
- var tableId = "graph_" + radioButton + "_table";
- var table = document.getElementById(tableId);
- table.style.display = "block";
- for (var i = 0; i < ids.length; i++) {
- if (ids[i] != tableId) {
- table = document.getElementById(ids[i]);
- table.style.display = "none";
- }
- }
- this._restoreNodes();
- if (radioButton == "R") {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
- }
- else if (radioButton == "H") {
- if (this.constants.hierarchicalLayout.enabled == false) {
- this.constants.hierarchicalLayout.enabled = true;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this.constants.physics.barnesHut.enabled = false;
- this._setupHierarchicalLayout();
+ Hammer.gestures.Release = {
+ name: 'release',
+ index: Infinity,
+ handler: function releaseGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ inst.trigger(this.name, ev);
+ }
}
- }
- else {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = true;
- }
- this._loadSelectedForceSolver();
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
- this.moving = true;
- this.start();
- }
-
-
- /**
- * this generates the ranges depending on the iniital values.
- *
- * @param id
- * @param map
- * @param constantsVariableName
- */
- function showValueOfRange (id,map,constantsVariableName) {
- var valueId = id + "_value";
- var rangeValue = document.getElementById(id).value;
-
- if (map instanceof Array) {
- document.getElementById(valueId).value = map[parseInt(rangeValue)];
- this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
- }
- else {
- document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
- this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
- }
+ };
- if (constantsVariableName == "hierarchicalLayout_direction" ||
- constantsVariableName == "hierarchicalLayout_levelSeparation" ||
- constantsVariableName == "hierarchicalLayout_nodeSpacing") {
- this._setupHierarchicalLayout();
- }
- this.moving = true;
- this.start();
+ // node export
+ if(typeof module === 'object' && typeof module.exports === 'object'){
+ module.exports = Hammer;
}
+ // just window export
+ else {
+ window.Hammer = Hammer;
+ // requireJS module definition
+ if(typeof window.define === 'function' && window.define.amd) {
+ window.define('hammer', [], function() {
+ return Hammer;
+ });
+ }
+ }
+ })(this);
/***/ },
/* 51 */
diff --git a/examples/network/index.html b/examples/network/index.html
index 3a555140..c360c653 100644
--- a/examples/network/index.html
+++ b/examples/network/index.html
@@ -37,6 +37,8 @@
23_hierarchical_layout.html
24_hierarchical_layout_userdefined.html
25_physics_configuration.html
+ 26_staticSmoothCurves.html
+ 27_world_cup_network.html
graphviz_gallery.html