UNPKG

1.04 MBJavaScriptView Raw
1/**
2 * vis.js
3 * https://github.com/almende/vis
4 *
5 * A dynamic, browser-based visualization library.
6 *
7 * @version 3.5.0
8 * @date 2014-09-16
9 *
10 * @license
11 * Copyright (C) 2011-2014 Almende B.V, http://almende.com
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
14 * use this file except in compliance with the License. You may obtain a copy
15 * of the License at
16 *
17 * http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
21 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
22 * License for the specific language governing permissions and limitations under
23 * the License.
24 */
25
26(function webpackUniversalModuleDefinition(root, factory) {
27 if(typeof exports === 'object' && typeof module === 'object')
28 module.exports = factory();
29 else if(typeof define === 'function' && define.amd)
30 define(factory);
31 else if(typeof exports === 'object')
32 exports["vis"] = factory();
33 else
34 root["vis"] = factory();
35})(this, function() {
36return /******/ (function(modules) { // webpackBootstrap
37/******/ // The module cache
38/******/ var installedModules = {};
39/******/
40/******/ // The require function
41/******/ function __webpack_require__(moduleId) {
42/******/
43/******/ // Check if module is in cache
44/******/ if(installedModules[moduleId])
45/******/ return installedModules[moduleId].exports;
46/******/
47/******/ // Create a new module (and put it into the cache)
48/******/ var module = installedModules[moduleId] = {
49/******/ exports: {},
50/******/ id: moduleId,
51/******/ loaded: false
52/******/ };
53/******/
54/******/ // Execute the module function
55/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
56/******/
57/******/ // Flag the module as loaded
58/******/ module.loaded = true;
59/******/
60/******/ // Return the exports of the module
61/******/ return module.exports;
62/******/ }
63/******/
64/******/
65/******/ // expose the modules object (__webpack_modules__)
66/******/ __webpack_require__.m = modules;
67/******/
68/******/ // expose the module cache
69/******/ __webpack_require__.c = installedModules;
70/******/
71/******/ // __webpack_public_path__
72/******/ __webpack_require__.p = "";
73/******/
74/******/ // Load entry module and return exports
75/******/ return __webpack_require__(0);
76/******/ })
77/************************************************************************/
78/******/ ([
79/* 0 */
80/***/ function(module, exports, __webpack_require__) {
81
82 // utils
83 exports.util = __webpack_require__(1);
84 exports.DOMutil = __webpack_require__(2);
85
86 // data
87 exports.DataSet = __webpack_require__(3);
88 exports.DataView = __webpack_require__(4);
89
90 // Graph3d
91 exports.Graph3d = __webpack_require__(5);
92 exports.graph3d = {
93 Camera: __webpack_require__(6),
94 Filter: __webpack_require__(7),
95 Point2d: __webpack_require__(8),
96 Point3d: __webpack_require__(9),
97 Slider: __webpack_require__(10),
98 StepNumber: __webpack_require__(11)
99 };
100
101 // Timeline
102 exports.Timeline = __webpack_require__(12);
103 exports.Graph2d = __webpack_require__(13);
104 exports.timeline = {
105 DataStep: __webpack_require__(14),
106 Range: __webpack_require__(15),
107 stack: __webpack_require__(16),
108 TimeStep: __webpack_require__(17),
109
110 components: {
111 items: {
112 Item: __webpack_require__(28),
113 BackgroundItem: __webpack_require__(29),
114 BoxItem: __webpack_require__(30),
115 PointItem: __webpack_require__(31),
116 RangeItem: __webpack_require__(32)
117 },
118
119 Component: __webpack_require__(18),
120 CurrentTime: __webpack_require__(19),
121 CustomTime: __webpack_require__(20),
122 DataAxis: __webpack_require__(21),
123 GraphGroup: __webpack_require__(22),
124 Group: __webpack_require__(23),
125 ItemSet: __webpack_require__(24),
126 Legend: __webpack_require__(25),
127 LineGraph: __webpack_require__(26),
128 TimeAxis: __webpack_require__(27)
129 }
130 };
131
132 // Network
133 exports.Network = __webpack_require__(33);
134 exports.network = {
135 Edge: __webpack_require__(34),
136 Groups: __webpack_require__(35),
137 Images: __webpack_require__(36),
138 Node: __webpack_require__(37),
139 Popup: __webpack_require__(38),
140 dotparser: __webpack_require__(39),
141 gephiParser: __webpack_require__(40)
142 };
143
144 // Deprecated since v3.0.0
145 exports.Graph = function () {
146 throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
147 };
148
149 // bundled external libraries
150 exports.moment = __webpack_require__(41);
151 exports.hammer = __webpack_require__(42);
152
153
154/***/ },
155/* 1 */
156/***/ function(module, exports, __webpack_require__) {
157
158 // utility functions
159
160 // first check if moment.js is already loaded in the browser window, if so,
161 // use this instance. Else, load via commonjs.
162 var moment = __webpack_require__(41);
163
164 /**
165 * Test whether given object is a number
166 * @param {*} object
167 * @return {Boolean} isNumber
168 */
169 exports.isNumber = function(object) {
170 return (object instanceof Number || typeof object == 'number');
171 };
172
173 /**
174 * Test whether given object is a string
175 * @param {*} object
176 * @return {Boolean} isString
177 */
178 exports.isString = function(object) {
179 return (object instanceof String || typeof object == 'string');
180 };
181
182 /**
183 * Test whether given object is a Date, or a String containing a Date
184 * @param {Date | String} object
185 * @return {Boolean} isDate
186 */
187 exports.isDate = function(object) {
188 if (object instanceof Date) {
189 return true;
190 }
191 else if (exports.isString(object)) {
192 // test whether this string contains a date
193 var match = ASPDateRegex.exec(object);
194 if (match) {
195 return true;
196 }
197 else if (!isNaN(Date.parse(object))) {
198 return true;
199 }
200 }
201
202 return false;
203 };
204
205 /**
206 * Test whether given object is an instance of google.visualization.DataTable
207 * @param {*} object
208 * @return {Boolean} isDataTable
209 */
210 exports.isDataTable = function(object) {
211 return (typeof (google) !== 'undefined') &&
212 (google.visualization) &&
213 (google.visualization.DataTable) &&
214 (object instanceof google.visualization.DataTable);
215 };
216
217 /**
218 * Create a semi UUID
219 * source: http://stackoverflow.com/a/105074/1262753
220 * @return {String} uuid
221 */
222 exports.randomUUID = function() {
223 var S4 = function () {
224 return Math.floor(
225 Math.random() * 0x10000 /* 65536 */
226 ).toString(16);
227 };
228
229 return (
230 S4() + S4() + '-' +
231 S4() + '-' +
232 S4() + '-' +
233 S4() + '-' +
234 S4() + S4() + S4()
235 );
236 };
237
238 /**
239 * Extend object a with the properties of object b or a series of objects
240 * Only properties with defined values are copied
241 * @param {Object} a
242 * @param {... Object} b
243 * @return {Object} a
244 */
245 exports.extend = function (a, b) {
246 for (var i = 1, len = arguments.length; i < len; i++) {
247 var other = arguments[i];
248 for (var prop in other) {
249 if (other.hasOwnProperty(prop)) {
250 a[prop] = other[prop];
251 }
252 }
253 }
254
255 return a;
256 };
257
258 /**
259 * Extend object a with selected properties of object b or a series of objects
260 * Only properties with defined values are copied
261 * @param {Array.<String>} props
262 * @param {Object} a
263 * @param {... Object} b
264 * @return {Object} a
265 */
266 exports.selectiveExtend = function (props, a, b) {
267 if (!Array.isArray(props)) {
268 throw new Error('Array with property names expected as first argument');
269 }
270
271 for (var i = 2; i < arguments.length; i++) {
272 var other = arguments[i];
273
274 for (var p = 0; p < props.length; p++) {
275 var prop = props[p];
276 if (other.hasOwnProperty(prop)) {
277 a[prop] = other[prop];
278 }
279 }
280 }
281 return a;
282 };
283
284 /**
285 * Extend object a with selected properties of object b or a series of objects
286 * Only properties with defined values are copied
287 * @param {Array.<String>} props
288 * @param {Object} a
289 * @param {... Object} b
290 * @return {Object} a
291 */
292 exports.selectiveDeepExtend = function (props, a, b) {
293 // TODO: add support for Arrays to deepExtend
294 if (Array.isArray(b)) {
295 throw new TypeError('Arrays are not supported by deepExtend');
296 }
297 for (var i = 2; i < arguments.length; i++) {
298 var other = arguments[i];
299 for (var p = 0; p < props.length; p++) {
300 var prop = props[p];
301 if (other.hasOwnProperty(prop)) {
302 if (b[prop] && b[prop].constructor === Object) {
303 if (a[prop] === undefined) {
304 a[prop] = {};
305 }
306 if (a[prop].constructor === Object) {
307 exports.deepExtend(a[prop], b[prop]);
308 }
309 else {
310 a[prop] = b[prop];
311 }
312 } else if (Array.isArray(b[prop])) {
313 throw new TypeError('Arrays are not supported by deepExtend');
314 } else {
315 a[prop] = b[prop];
316 }
317
318 }
319 }
320 }
321 return a;
322 };
323
324 /**
325 * Extend object a with selected properties of object b or a series of objects
326 * Only properties with defined values are copied
327 * @param {Array.<String>} props
328 * @param {Object} a
329 * @param {... Object} b
330 * @return {Object} a
331 */
332 exports.selectiveNotDeepExtend = function (props, a, b) {
333 // TODO: add support for Arrays to deepExtend
334 if (Array.isArray(b)) {
335 throw new TypeError('Arrays are not supported by deepExtend');
336 }
337 for (var prop in b) {
338 if (b.hasOwnProperty(prop)) {
339 if (props.indexOf(prop) == -1) {
340 if (b[prop] && b[prop].constructor === Object) {
341 if (a[prop] === undefined) {
342 a[prop] = {};
343 }
344 if (a[prop].constructor === Object) {
345 exports.deepExtend(a[prop], b[prop]);
346 }
347 else {
348 a[prop] = b[prop];
349 }
350 } else if (Array.isArray(b[prop])) {
351 throw new TypeError('Arrays are not supported by deepExtend');
352 } else {
353 a[prop] = b[prop];
354 }
355 }
356 }
357 }
358 return a;
359 };
360
361 /**
362 * Deep extend an object a with the properties of object b
363 * @param {Object} a
364 * @param {Object} b
365 * @returns {Object}
366 */
367 exports.deepExtend = function(a, b) {
368 // TODO: add support for Arrays to deepExtend
369 if (Array.isArray(b)) {
370 throw new TypeError('Arrays are not supported by deepExtend');
371 }
372
373 for (var prop in b) {
374 if (b.hasOwnProperty(prop)) {
375 if (b[prop] && b[prop].constructor === Object) {
376 if (a[prop] === undefined) {
377 a[prop] = {};
378 }
379 if (a[prop].constructor === Object) {
380 exports.deepExtend(a[prop], b[prop]);
381 }
382 else {
383 a[prop] = b[prop];
384 }
385 } else if (Array.isArray(b[prop])) {
386 throw new TypeError('Arrays are not supported by deepExtend');
387 } else {
388 a[prop] = b[prop];
389 }
390 }
391 }
392 return a;
393 };
394
395 /**
396 * Test whether all elements in two arrays are equal.
397 * @param {Array} a
398 * @param {Array} b
399 * @return {boolean} Returns true if both arrays have the same length and same
400 * elements.
401 */
402 exports.equalArray = function (a, b) {
403 if (a.length != b.length) return false;
404
405 for (var i = 0, len = a.length; i < len; i++) {
406 if (a[i] != b[i]) return false;
407 }
408
409 return true;
410 };
411
412 /**
413 * Convert an object to another type
414 * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
415 * @param {String | undefined} type Name of the type. Available types:
416 * 'Boolean', 'Number', 'String',
417 * 'Date', 'Moment', ISODate', 'ASPDate'.
418 * @return {*} object
419 * @throws Error
420 */
421 exports.convert = function(object, type) {
422 var match;
423
424 if (object === undefined) {
425 return undefined;
426 }
427 if (object === null) {
428 return null;
429 }
430
431 if (!type) {
432 return object;
433 }
434 if (!(typeof type === 'string') && !(type instanceof String)) {
435 throw new Error('Type must be a string');
436 }
437
438 //noinspection FallthroughInSwitchStatementJS
439 switch (type) {
440 case 'boolean':
441 case 'Boolean':
442 return Boolean(object);
443
444 case 'number':
445 case 'Number':
446 return Number(object.valueOf());
447
448 case 'string':
449 case 'String':
450 return String(object);
451
452 case 'Date':
453 if (exports.isNumber(object)) {
454 return new Date(object);
455 }
456 if (object instanceof Date) {
457 return new Date(object.valueOf());
458 }
459 else if (moment.isMoment(object)) {
460 return new Date(object.valueOf());
461 }
462 if (exports.isString(object)) {
463 match = ASPDateRegex.exec(object);
464 if (match) {
465 // object is an ASP date
466 return new Date(Number(match[1])); // parse number
467 }
468 else {
469 return moment(object).toDate(); // parse string
470 }
471 }
472 else {
473 throw new Error(
474 'Cannot convert object of type ' + exports.getType(object) +
475 ' to type Date');
476 }
477
478 case 'Moment':
479 if (exports.isNumber(object)) {
480 return moment(object);
481 }
482 if (object instanceof Date) {
483 return moment(object.valueOf());
484 }
485 else if (moment.isMoment(object)) {
486 return moment(object);
487 }
488 if (exports.isString(object)) {
489 match = ASPDateRegex.exec(object);
490 if (match) {
491 // object is an ASP date
492 return moment(Number(match[1])); // parse number
493 }
494 else {
495 return moment(object); // parse string
496 }
497 }
498 else {
499 throw new Error(
500 'Cannot convert object of type ' + exports.getType(object) +
501 ' to type Date');
502 }
503
504 case 'ISODate':
505 if (exports.isNumber(object)) {
506 return new Date(object);
507 }
508 else if (object instanceof Date) {
509 return object.toISOString();
510 }
511 else if (moment.isMoment(object)) {
512 return object.toDate().toISOString();
513 }
514 else if (exports.isString(object)) {
515 match = ASPDateRegex.exec(object);
516 if (match) {
517 // object is an ASP date
518 return new Date(Number(match[1])).toISOString(); // parse number
519 }
520 else {
521 return new Date(object).toISOString(); // parse string
522 }
523 }
524 else {
525 throw new Error(
526 'Cannot convert object of type ' + exports.getType(object) +
527 ' to type ISODate');
528 }
529
530 case 'ASPDate':
531 if (exports.isNumber(object)) {
532 return '/Date(' + object + ')/';
533 }
534 else if (object instanceof Date) {
535 return '/Date(' + object.valueOf() + ')/';
536 }
537 else if (exports.isString(object)) {
538 match = ASPDateRegex.exec(object);
539 var value;
540 if (match) {
541 // object is an ASP date
542 value = new Date(Number(match[1])).valueOf(); // parse number
543 }
544 else {
545 value = new Date(object).valueOf(); // parse string
546 }
547 return '/Date(' + value + ')/';
548 }
549 else {
550 throw new Error(
551 'Cannot convert object of type ' + exports.getType(object) +
552 ' to type ASPDate');
553 }
554
555 default:
556 throw new Error('Unknown type "' + type + '"');
557 }
558 };
559
560 // parse ASP.Net Date pattern,
561 // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
562 // code from http://momentjs.com/
563 var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
564
565 /**
566 * Get the type of an object, for example exports.getType([]) returns 'Array'
567 * @param {*} object
568 * @return {String} type
569 */
570 exports.getType = function(object) {
571 var type = typeof object;
572
573 if (type == 'object') {
574 if (object == null) {
575 return 'null';
576 }
577 if (object instanceof Boolean) {
578 return 'Boolean';
579 }
580 if (object instanceof Number) {
581 return 'Number';
582 }
583 if (object instanceof String) {
584 return 'String';
585 }
586 if (object instanceof Array) {
587 return 'Array';
588 }
589 if (object instanceof Date) {
590 return 'Date';
591 }
592 return 'Object';
593 }
594 else if (type == 'number') {
595 return 'Number';
596 }
597 else if (type == 'boolean') {
598 return 'Boolean';
599 }
600 else if (type == 'string') {
601 return 'String';
602 }
603
604 return type;
605 };
606
607 /**
608 * Retrieve the absolute left value of a DOM element
609 * @param {Element} elem A dom element, for example a div
610 * @return {number} left The absolute left position of this element
611 * in the browser page.
612 */
613 exports.getAbsoluteLeft = function(elem) {
614 return elem.getBoundingClientRect().left + window.pageXOffset;
615 };
616
617 /**
618 * Retrieve the absolute top value of a DOM element
619 * @param {Element} elem A dom element, for example a div
620 * @return {number} top The absolute top position of this element
621 * in the browser page.
622 */
623 exports.getAbsoluteTop = function(elem) {
624 return elem.getBoundingClientRect().top + window.pageYOffset;
625 };
626
627 /**
628 * add a className to the given elements style
629 * @param {Element} elem
630 * @param {String} className
631 */
632 exports.addClassName = function(elem, className) {
633 var classes = elem.className.split(' ');
634 if (classes.indexOf(className) == -1) {
635 classes.push(className); // add the class to the array
636 elem.className = classes.join(' ');
637 }
638 };
639
640 /**
641 * add a className to the given elements style
642 * @param {Element} elem
643 * @param {String} className
644 */
645 exports.removeClassName = function(elem, className) {
646 var classes = elem.className.split(' ');
647 var index = classes.indexOf(className);
648 if (index != -1) {
649 classes.splice(index, 1); // remove the class from the array
650 elem.className = classes.join(' ');
651 }
652 };
653
654 /**
655 * For each method for both arrays and objects.
656 * In case of an array, the built-in Array.forEach() is applied.
657 * In case of an Object, the method loops over all properties of the object.
658 * @param {Object | Array} object An Object or Array
659 * @param {function} callback Callback method, called for each item in
660 * the object or array with three parameters:
661 * callback(value, index, object)
662 */
663 exports.forEach = function(object, callback) {
664 var i,
665 len;
666 if (object instanceof Array) {
667 // array
668 for (i = 0, len = object.length; i < len; i++) {
669 callback(object[i], i, object);
670 }
671 }
672 else {
673 // object
674 for (i in object) {
675 if (object.hasOwnProperty(i)) {
676 callback(object[i], i, object);
677 }
678 }
679 }
680 };
681
682 /**
683 * Convert an object into an array: all objects properties are put into the
684 * array. The resulting array is unordered.
685 * @param {Object} object
686 * @param {Array} array
687 */
688 exports.toArray = function(object) {
689 var array = [];
690
691 for (var prop in object) {
692 if (object.hasOwnProperty(prop)) array.push(object[prop]);
693 }
694
695 return array;
696 }
697
698 /**
699 * Update a property in an object
700 * @param {Object} object
701 * @param {String} key
702 * @param {*} value
703 * @return {Boolean} changed
704 */
705 exports.updateProperty = function(object, key, value) {
706 if (object[key] !== value) {
707 object[key] = value;
708 return true;
709 }
710 else {
711 return false;
712 }
713 };
714
715 /**
716 * Add and event listener. Works for all browsers
717 * @param {Element} element An html element
718 * @param {string} action The action, for example "click",
719 * without the prefix "on"
720 * @param {function} listener The callback function to be executed
721 * @param {boolean} [useCapture]
722 */
723 exports.addEventListener = function(element, action, listener, useCapture) {
724 if (element.addEventListener) {
725 if (useCapture === undefined)
726 useCapture = false;
727
728 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
729 action = "DOMMouseScroll"; // For Firefox
730 }
731
732 element.addEventListener(action, listener, useCapture);
733 } else {
734 element.attachEvent("on" + action, listener); // IE browsers
735 }
736 };
737
738 /**
739 * Remove an event listener from an element
740 * @param {Element} element An html dom element
741 * @param {string} action The name of the event, for example "mousedown"
742 * @param {function} listener The listener function
743 * @param {boolean} [useCapture]
744 */
745 exports.removeEventListener = function(element, action, listener, useCapture) {
746 if (element.removeEventListener) {
747 // non-IE browsers
748 if (useCapture === undefined)
749 useCapture = false;
750
751 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
752 action = "DOMMouseScroll"; // For Firefox
753 }
754
755 element.removeEventListener(action, listener, useCapture);
756 } else {
757 // IE browsers
758 element.detachEvent("on" + action, listener);
759 }
760 };
761
762 /**
763 * Cancels the event if it is cancelable, without stopping further propagation of the event.
764 */
765 exports.preventDefault = function (event) {
766 if (!event)
767 event = window.event;
768
769 if (event.preventDefault) {
770 event.preventDefault(); // non-IE browsers
771 }
772 else {
773 event.returnValue = false; // IE browsers
774 }
775 };
776
777 /**
778 * Get HTML element which is the target of the event
779 * @param {Event} event
780 * @return {Element} target element
781 */
782 exports.getTarget = function(event) {
783 // code from http://www.quirksmode.org/js/events_properties.html
784 if (!event) {
785 event = window.event;
786 }
787
788 var target;
789
790 if (event.target) {
791 target = event.target;
792 }
793 else if (event.srcElement) {
794 target = event.srcElement;
795 }
796
797 if (target.nodeType != undefined && target.nodeType == 3) {
798 // defeat Safari bug
799 target = target.parentNode;
800 }
801
802 return target;
803 };
804
805 exports.option = {};
806
807 /**
808 * Convert a value into a boolean
809 * @param {Boolean | function | undefined} value
810 * @param {Boolean} [defaultValue]
811 * @returns {Boolean} bool
812 */
813 exports.option.asBoolean = function (value, defaultValue) {
814 if (typeof value == 'function') {
815 value = value();
816 }
817
818 if (value != null) {
819 return (value != false);
820 }
821
822 return defaultValue || null;
823 };
824
825 /**
826 * Convert a value into a number
827 * @param {Boolean | function | undefined} value
828 * @param {Number} [defaultValue]
829 * @returns {Number} number
830 */
831 exports.option.asNumber = function (value, defaultValue) {
832 if (typeof value == 'function') {
833 value = value();
834 }
835
836 if (value != null) {
837 return Number(value) || defaultValue || null;
838 }
839
840 return defaultValue || null;
841 };
842
843 /**
844 * Convert a value into a string
845 * @param {String | function | undefined} value
846 * @param {String} [defaultValue]
847 * @returns {String} str
848 */
849 exports.option.asString = function (value, defaultValue) {
850 if (typeof value == 'function') {
851 value = value();
852 }
853
854 if (value != null) {
855 return String(value);
856 }
857
858 return defaultValue || null;
859 };
860
861 /**
862 * Convert a size or location into a string with pixels or a percentage
863 * @param {String | Number | function | undefined} value
864 * @param {String} [defaultValue]
865 * @returns {String} size
866 */
867 exports.option.asSize = function (value, defaultValue) {
868 if (typeof value == 'function') {
869 value = value();
870 }
871
872 if (exports.isString(value)) {
873 return value;
874 }
875 else if (exports.isNumber(value)) {
876 return value + 'px';
877 }
878 else {
879 return defaultValue || null;
880 }
881 };
882
883 /**
884 * Convert a value into a DOM element
885 * @param {HTMLElement | function | undefined} value
886 * @param {HTMLElement} [defaultValue]
887 * @returns {HTMLElement | null} dom
888 */
889 exports.option.asElement = function (value, defaultValue) {
890 if (typeof value == 'function') {
891 value = value();
892 }
893
894 return value || defaultValue || null;
895 };
896
897
898
899 exports.GiveDec = function(Hex) {
900 var Value;
901
902 if (Hex == "A")
903 Value = 10;
904 else if (Hex == "B")
905 Value = 11;
906 else if (Hex == "C")
907 Value = 12;
908 else if (Hex == "D")
909 Value = 13;
910 else if (Hex == "E")
911 Value = 14;
912 else if (Hex == "F")
913 Value = 15;
914 else
915 Value = eval(Hex);
916
917 return Value;
918 };
919
920 exports.GiveHex = function(Dec) {
921 var Value;
922
923 if(Dec == 10)
924 Value = "A";
925 else if (Dec == 11)
926 Value = "B";
927 else if (Dec == 12)
928 Value = "C";
929 else if (Dec == 13)
930 Value = "D";
931 else if (Dec == 14)
932 Value = "E";
933 else if (Dec == 15)
934 Value = "F";
935 else
936 Value = "" + Dec;
937
938 return Value;
939 };
940
941 /**
942 * Parse a color property into an object with border, background, and
943 * highlight colors
944 * @param {Object | String} color
945 * @return {Object} colorObject
946 */
947 exports.parseColor = function(color) {
948 var c;
949 if (exports.isString(color)) {
950 if (exports.isValidRGB(color)) {
951 var rgb = color.substr(4).substr(0,color.length-5).split(',');
952 color = exports.RGBToHex(rgb[0],rgb[1],rgb[2]);
953 }
954 if (exports.isValidHex(color)) {
955 var hsv = exports.hexToHSV(color);
956 var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
957 var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
958 var darkerColorHex = exports.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
959 var lighterColorHex = exports.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
960
961 c = {
962 background: color,
963 border:darkerColorHex,
964 highlight: {
965 background:lighterColorHex,
966 border:darkerColorHex
967 },
968 hover: {
969 background:lighterColorHex,
970 border:darkerColorHex
971 }
972 };
973 }
974 else {
975 c = {
976 background:color,
977 border:color,
978 highlight: {
979 background:color,
980 border:color
981 },
982 hover: {
983 background:color,
984 border:color
985 }
986 };
987 }
988 }
989 else {
990 c = {};
991 c.background = color.background || 'white';
992 c.border = color.border || c.background;
993
994 if (exports.isString(color.highlight)) {
995 c.highlight = {
996 border: color.highlight,
997 background: color.highlight
998 }
999 }
1000 else {
1001 c.highlight = {};
1002 c.highlight.background = color.highlight && color.highlight.background || c.background;
1003 c.highlight.border = color.highlight && color.highlight.border || c.border;
1004 }
1005
1006 if (exports.isString(color.hover)) {
1007 c.hover = {
1008 border: color.hover,
1009 background: color.hover
1010 }
1011 }
1012 else {
1013 c.hover = {};
1014 c.hover.background = color.hover && color.hover.background || c.background;
1015 c.hover.border = color.hover && color.hover.border || c.border;
1016 }
1017 }
1018
1019 return c;
1020 };
1021
1022 /**
1023 * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
1024 *
1025 * @param {String} hex
1026 * @returns {{r: *, g: *, b: *}}
1027 */
1028 exports.hexToRGB = function(hex) {
1029 hex = hex.replace("#","").toUpperCase();
1030
1031 var a = exports.GiveDec(hex.substring(0, 1));
1032 var b = exports.GiveDec(hex.substring(1, 2));
1033 var c = exports.GiveDec(hex.substring(2, 3));
1034 var d = exports.GiveDec(hex.substring(3, 4));
1035 var e = exports.GiveDec(hex.substring(4, 5));
1036 var f = exports.GiveDec(hex.substring(5, 6));
1037
1038 var r = (a * 16) + b;
1039 var g = (c * 16) + d;
1040 var b = (e * 16) + f;
1041
1042 return {r:r,g:g,b:b};
1043 };
1044
1045 exports.RGBToHex = function(red,green,blue) {
1046 var a = exports.GiveHex(Math.floor(red / 16));
1047 var b = exports.GiveHex(red % 16);
1048 var c = exports.GiveHex(Math.floor(green / 16));
1049 var d = exports.GiveHex(green % 16);
1050 var e = exports.GiveHex(Math.floor(blue / 16));
1051 var f = exports.GiveHex(blue % 16);
1052
1053 var hex = a + b + c + d + e + f;
1054 return "#" + hex;
1055 };
1056
1057
1058 /**
1059 * http://www.javascripter.net/faq/rgb2hsv.htm
1060 *
1061 * @param red
1062 * @param green
1063 * @param blue
1064 * @returns {*}
1065 * @constructor
1066 */
1067 exports.RGBToHSV = function(red,green,blue) {
1068 red=red/255; green=green/255; blue=blue/255;
1069 var minRGB = Math.min(red,Math.min(green,blue));
1070 var maxRGB = Math.max(red,Math.max(green,blue));
1071
1072 // Black-gray-white
1073 if (minRGB == maxRGB) {
1074 return {h:0,s:0,v:minRGB};
1075 }
1076
1077 // Colors other than black-gray-white:
1078 var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
1079 var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
1080 var hue = 60*(h - d/(maxRGB - minRGB))/360;
1081 var saturation = (maxRGB - minRGB)/maxRGB;
1082 var value = maxRGB;
1083 return {h:hue,s:saturation,v:value};
1084 };
1085
1086
1087 /**
1088 * https://gist.github.com/mjijackson/5311256
1089 * @param h
1090 * @param s
1091 * @param v
1092 * @returns {{r: number, g: number, b: number}}
1093 * @constructor
1094 */
1095 exports.HSVToRGB = function(h, s, v) {
1096 var r, g, b;
1097
1098 var i = Math.floor(h * 6);
1099 var f = h * 6 - i;
1100 var p = v * (1 - s);
1101 var q = v * (1 - f * s);
1102 var t = v * (1 - (1 - f) * s);
1103
1104 switch (i % 6) {
1105 case 0: r = v, g = t, b = p; break;
1106 case 1: r = q, g = v, b = p; break;
1107 case 2: r = p, g = v, b = t; break;
1108 case 3: r = p, g = q, b = v; break;
1109 case 4: r = t, g = p, b = v; break;
1110 case 5: r = v, g = p, b = q; break;
1111 }
1112
1113 return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
1114 };
1115
1116 exports.HSVToHex = function(h, s, v) {
1117 var rgb = exports.HSVToRGB(h, s, v);
1118 return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
1119 };
1120
1121 exports.hexToHSV = function(hex) {
1122 var rgb = exports.hexToRGB(hex);
1123 return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
1124 };
1125
1126 exports.isValidHex = function(hex) {
1127 var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
1128 return isOk;
1129 };
1130
1131 exports.isValidRGB = function(rgb) {
1132 rgb = rgb.replace(" ","");
1133 var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
1134 return isOk;
1135 }
1136
1137 /**
1138 * This recursively redirects the prototype of JSON objects to the referenceObject
1139 * This is used for default options.
1140 *
1141 * @param referenceObject
1142 * @returns {*}
1143 */
1144 exports.selectiveBridgeObject = function(fields, referenceObject) {
1145 if (typeof referenceObject == "object") {
1146 var objectTo = Object.create(referenceObject);
1147 for (var i = 0; i < fields.length; i++) {
1148 if (referenceObject.hasOwnProperty(fields[i])) {
1149 if (typeof referenceObject[fields[i]] == "object") {
1150 objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
1151 }
1152 }
1153 }
1154 return objectTo;
1155 }
1156 else {
1157 return null;
1158 }
1159 };
1160
1161 /**
1162 * This recursively redirects the prototype of JSON objects to the referenceObject
1163 * This is used for default options.
1164 *
1165 * @param referenceObject
1166 * @returns {*}
1167 */
1168 exports.bridgeObject = function(referenceObject) {
1169 if (typeof referenceObject == "object") {
1170 var objectTo = Object.create(referenceObject);
1171 for (var i in referenceObject) {
1172 if (referenceObject.hasOwnProperty(i)) {
1173 if (typeof referenceObject[i] == "object") {
1174 objectTo[i] = exports.bridgeObject(referenceObject[i]);
1175 }
1176 }
1177 }
1178 return objectTo;
1179 }
1180 else {
1181 return null;
1182 }
1183 };
1184
1185
1186 /**
1187 * this is used to set the options of subobjects in the options object. A requirement of these subobjects
1188 * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
1189 *
1190 * @param [object] mergeTarget | this is either this.options or the options used for the groups.
1191 * @param [object] options | options
1192 * @param [String] option | this is the option key in the options argument
1193 * @private
1194 */
1195 exports.mergeOptions = function (mergeTarget, options, option) {
1196 if (options[option] !== undefined) {
1197 if (typeof options[option] == 'boolean') {
1198 mergeTarget[option].enabled = options[option];
1199 }
1200 else {
1201 mergeTarget[option].enabled = true;
1202 for (prop in options[option]) {
1203 if (options[option].hasOwnProperty(prop)) {
1204 mergeTarget[option][prop] = options[option][prop];
1205 }
1206 }
1207 }
1208 }
1209 }
1210
1211
1212 /**
1213 * this is used to set the options of subobjects in the options object. A requirement of these subobjects
1214 * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
1215 *
1216 * @param [object] mergeTarget | this is either this.options or the options used for the groups.
1217 * @param [object] options | options
1218 * @param [String] option | this is the option key in the options argument
1219 * @private
1220 */
1221 exports.mergeOptions = function (mergeTarget, options, option) {
1222 if (options[option] !== undefined) {
1223 if (typeof options[option] == 'boolean') {
1224 mergeTarget[option].enabled = options[option];
1225 }
1226 else {
1227 mergeTarget[option].enabled = true;
1228 for (prop in options[option]) {
1229 if (options[option].hasOwnProperty(prop)) {
1230 mergeTarget[option][prop] = options[option][prop];
1231 }
1232 }
1233 }
1234 }
1235 }
1236
1237
1238
1239
1240 /**
1241 * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
1242 * arrays. This is done by giving a boolean value true if you want to use the byEnd.
1243 * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
1244 * if the time we selected (start or end) is within the current range).
1245 *
1246 * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the RangeItem that is
1247 * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
1248 * either the start OR end time has to be in the range.
1249 *
1250 * @param {Item[]} orderedItems Items ordered by start
1251 * @param {{start: number, end: number}} range
1252 * @param {String} field
1253 * @param {String} field2
1254 * @returns {number}
1255 * @private
1256 */
1257 exports.binarySearch = function(orderedItems, range, field, field2) {
1258 var array = orderedItems;
1259
1260 var maxIterations = 10000;
1261 var iteration = 0;
1262 var found = false;
1263 var low = 0;
1264 var high = array.length;
1265 var newLow = low;
1266 var newHigh = high;
1267 var guess = Math.floor(0.5*(high+low));
1268 var value;
1269
1270 if (high == 0) {
1271 guess = -1;
1272 }
1273 else if (high == 1) {
1274 if (array[guess].isVisible(range)) {
1275 guess = 0;
1276 }
1277 else {
1278 guess = -1;
1279 }
1280 }
1281 else {
1282 high -= 1;
1283
1284 while (found == false && iteration < maxIterations) {
1285 value = field2 === undefined ? array[guess][field] : array[guess][field][field2];
1286
1287 if (array[guess].isVisible(range)) {
1288 found = true;
1289 }
1290 else {
1291 if (value < range.start) { // it is too small --> increase low
1292 newLow = Math.floor(0.5*(high+low));
1293 }
1294 else { // it is too big --> decrease high
1295 newHigh = Math.floor(0.5*(high+low));
1296 }
1297 // not in list;
1298 if (low == newLow && high == newHigh) {
1299 guess = -1;
1300 found = true;
1301 }
1302 else {
1303 high = newHigh; low = newLow;
1304 guess = Math.floor(0.5*(high+low));
1305 }
1306 }
1307 iteration++;
1308 }
1309 if (iteration >= maxIterations) {
1310 console.log("BinarySearch too many iterations. Aborting.");
1311 }
1312 }
1313 return guess;
1314 };
1315
1316 /**
1317 * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
1318 * arrays. This is done by giving a boolean value true if you want to use the byEnd.
1319 * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
1320 * if the time we selected (start or end) is within the current range).
1321 *
1322 * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the RangeItem that is
1323 * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
1324 * either the start OR end time has to be in the range.
1325 *
1326 * @param {Array} orderedItems
1327 * @param {{start: number, end: number}} target
1328 * @param {String} field
1329 * @param {String} sidePreference 'before' or 'after'
1330 * @returns {number}
1331 * @private
1332 */
1333 exports.binarySearchGeneric = function(orderedItems, target, field, sidePreference) {
1334 var maxIterations = 10000;
1335 var iteration = 0;
1336 var array = orderedItems;
1337 var found = false;
1338 var low = 0;
1339 var high = array.length;
1340 var newLow = low;
1341 var newHigh = high;
1342 var guess = Math.floor(0.5*(high+low));
1343 var newGuess;
1344 var prevValue, value, nextValue;
1345
1346 if (high == 0) {guess = -1;}
1347 else if (high == 1) {
1348 value = array[guess][field];
1349 if (value == target) {
1350 guess = 0;
1351 }
1352 else {
1353 guess = -1;
1354 }
1355 }
1356 else {
1357 high -= 1;
1358 while (found == false && iteration < maxIterations) {
1359 prevValue = array[Math.max(0,guess - 1)][field];
1360 value = array[guess][field];
1361 nextValue = array[Math.min(array.length-1,guess + 1)][field];
1362
1363 if (value == target || prevValue < target && value > target || value < target && nextValue > target) {
1364 found = true;
1365 if (value != target) {
1366 if (sidePreference == 'before') {
1367 if (prevValue < target && value > target) {
1368 guess = Math.max(0,guess - 1);
1369 }
1370 }
1371 else {
1372 if (value < target && nextValue > target) {
1373 guess = Math.min(array.length-1,guess + 1);
1374 }
1375 }
1376 }
1377 }
1378 else {
1379 if (value < target) { // it is too small --> increase low
1380 newLow = Math.floor(0.5*(high+low));
1381 }
1382 else { // it is too big --> decrease high
1383 newHigh = Math.floor(0.5*(high+low));
1384 }
1385 newGuess = Math.floor(0.5*(high+low));
1386 // not in list;
1387 if (low == newLow && high == newHigh) {
1388 guess = -1;
1389 found = true;
1390 }
1391 else {
1392 high = newHigh; low = newLow;
1393 guess = Math.floor(0.5*(high+low));
1394 }
1395 }
1396 iteration++;
1397 }
1398 if (iteration >= maxIterations) {
1399 console.log("BinarySearch too many iterations. Aborting.");
1400 }
1401 }
1402 return guess;
1403 };
1404
1405 /**
1406 * Quadratic ease-in-out
1407 * http://gizma.com/easing/
1408 * @param {number} t Current time
1409 * @param {number} start Start value
1410 * @param {number} end End value
1411 * @param {number} duration Duration
1412 * @returns {number} Value corresponding with current time
1413 */
1414 exports.easeInOutQuad = function (t, start, end, duration) {
1415 var change = end - start;
1416 t /= duration/2;
1417 if (t < 1) return change/2*t*t + start;
1418 t--;
1419 return -change/2 * (t*(t-2) - 1) + start;
1420 };
1421
1422
1423
1424 /*
1425 * Easing Functions - inspired from http://gizma.com/easing/
1426 * only considering the t value for the range [0, 1] => [0, 1]
1427 * https://gist.github.com/gre/1650294
1428 */
1429 exports.easingFunctions = {
1430 // no easing, no acceleration
1431 linear: function (t) {
1432 return t
1433 },
1434 // accelerating from zero velocity
1435 easeInQuad: function (t) {
1436 return t * t
1437 },
1438 // decelerating to zero velocity
1439 easeOutQuad: function (t) {
1440 return t * (2 - t)
1441 },
1442 // acceleration until halfway, then deceleration
1443 easeInOutQuad: function (t) {
1444 return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
1445 },
1446 // accelerating from zero velocity
1447 easeInCubic: function (t) {
1448 return t * t * t
1449 },
1450 // decelerating to zero velocity
1451 easeOutCubic: function (t) {
1452 return (--t) * t * t + 1
1453 },
1454 // acceleration until halfway, then deceleration
1455 easeInOutCubic: function (t) {
1456 return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
1457 },
1458 // accelerating from zero velocity
1459 easeInQuart: function (t) {
1460 return t * t * t * t
1461 },
1462 // decelerating to zero velocity
1463 easeOutQuart: function (t) {
1464 return 1 - (--t) * t * t * t
1465 },
1466 // acceleration until halfway, then deceleration
1467 easeInOutQuart: function (t) {
1468 return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
1469 },
1470 // accelerating from zero velocity
1471 easeInQuint: function (t) {
1472 return t * t * t * t * t
1473 },
1474 // decelerating to zero velocity
1475 easeOutQuint: function (t) {
1476 return 1 + (--t) * t * t * t * t
1477 },
1478 // acceleration until halfway, then deceleration
1479 easeInOutQuint: function (t) {
1480 return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
1481 }
1482 };
1483
1484/***/ },
1485/* 2 */
1486/***/ function(module, exports, __webpack_require__) {
1487
1488 // DOM utility methods
1489
1490 /**
1491 * this prepares the JSON container for allocating SVG elements
1492 * @param JSONcontainer
1493 * @private
1494 */
1495 exports.prepareElements = function(JSONcontainer) {
1496 // cleanup the redundant svgElements;
1497 for (var elementType in JSONcontainer) {
1498 if (JSONcontainer.hasOwnProperty(elementType)) {
1499 JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
1500 JSONcontainer[elementType].used = [];
1501 }
1502 }
1503 };
1504
1505 /**
1506 * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
1507 * which to remove the redundant elements.
1508 *
1509 * @param JSONcontainer
1510 * @private
1511 */
1512 exports.cleanupElements = function(JSONcontainer) {
1513 // cleanup the redundant svgElements;
1514 for (var elementType in JSONcontainer) {
1515 if (JSONcontainer.hasOwnProperty(elementType)) {
1516 if (JSONcontainer[elementType].redundant) {
1517 for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
1518 JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
1519 }
1520 JSONcontainer[elementType].redundant = [];
1521 }
1522 }
1523 }
1524 };
1525
1526 /**
1527 * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
1528 * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
1529 *
1530 * @param elementType
1531 * @param JSONcontainer
1532 * @param svgContainer
1533 * @returns {*}
1534 * @private
1535 */
1536 exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
1537 var element;
1538 // allocate SVG element, if it doesnt yet exist, create one.
1539 if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
1540 // check if there is an redundant element
1541 if (JSONcontainer[elementType].redundant.length > 0) {
1542 element = JSONcontainer[elementType].redundant[0];
1543 JSONcontainer[elementType].redundant.shift();
1544 }
1545 else {
1546 // create a new element and add it to the SVG
1547 element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
1548 svgContainer.appendChild(element);
1549 }
1550 }
1551 else {
1552 // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
1553 element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
1554 JSONcontainer[elementType] = {used: [], redundant: []};
1555 svgContainer.appendChild(element);
1556 }
1557 JSONcontainer[elementType].used.push(element);
1558 return element;
1559 };
1560
1561
1562 /**
1563 * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
1564 * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
1565 *
1566 * @param elementType
1567 * @param JSONcontainer
1568 * @param DOMContainer
1569 * @returns {*}
1570 * @private
1571 */
1572 exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) {
1573 var element;
1574 // allocate DOM element, if it doesnt yet exist, create one.
1575 if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
1576 // check if there is an redundant element
1577 if (JSONcontainer[elementType].redundant.length > 0) {
1578 element = JSONcontainer[elementType].redundant[0];
1579 JSONcontainer[elementType].redundant.shift();
1580 }
1581 else {
1582 // create a new element and add it to the SVG
1583 element = document.createElement(elementType);
1584 if (insertBefore !== undefined) {
1585 DOMContainer.insertBefore(element, insertBefore);
1586 }
1587 else {
1588 DOMContainer.appendChild(element);
1589 }
1590 }
1591 }
1592 else {
1593 // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
1594 element = document.createElement(elementType);
1595 JSONcontainer[elementType] = {used: [], redundant: []};
1596 if (insertBefore !== undefined) {
1597 DOMContainer.insertBefore(element, insertBefore);
1598 }
1599 else {
1600 DOMContainer.appendChild(element);
1601 }
1602 }
1603 JSONcontainer[elementType].used.push(element);
1604 return element;
1605 };
1606
1607
1608
1609
1610 /**
1611 * draw a point object. this is a seperate function because it can also be called by the legend.
1612 * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
1613 * as well.
1614 *
1615 * @param x
1616 * @param y
1617 * @param group
1618 * @param JSONcontainer
1619 * @param svgContainer
1620 * @returns {*}
1621 */
1622 exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
1623 var point;
1624 if (group.options.drawPoints.style == 'circle') {
1625 point = exports.getSVGElement('circle',JSONcontainer,svgContainer);
1626 point.setAttributeNS(null, "cx", x);
1627 point.setAttributeNS(null, "cy", y);
1628 point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size);
1629 point.setAttributeNS(null, "class", group.className + " point");
1630 }
1631 else {
1632 point = exports.getSVGElement('rect',JSONcontainer,svgContainer);
1633 point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size);
1634 point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size);
1635 point.setAttributeNS(null, "width", group.options.drawPoints.size);
1636 point.setAttributeNS(null, "height", group.options.drawPoints.size);
1637 point.setAttributeNS(null, "class", group.className + " point");
1638 }
1639 return point;
1640 };
1641
1642 /**
1643 * draw a bar SVG element centered on the X coordinate
1644 *
1645 * @param x
1646 * @param y
1647 * @param className
1648 */
1649 exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
1650 // if (height != 0) {
1651 var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer);
1652 rect.setAttributeNS(null, "x", x - 0.5 * width);
1653 rect.setAttributeNS(null, "y", y);
1654 rect.setAttributeNS(null, "width", width);
1655 rect.setAttributeNS(null, "height", height);
1656 rect.setAttributeNS(null, "class", className);
1657 // }
1658 };
1659
1660/***/ },
1661/* 3 */
1662/***/ function(module, exports, __webpack_require__) {
1663
1664 var util = __webpack_require__(1);
1665
1666 /**
1667 * DataSet
1668 *
1669 * Usage:
1670 * var dataSet = new DataSet({
1671 * fieldId: '_id',
1672 * type: {
1673 * // ...
1674 * }
1675 * });
1676 *
1677 * dataSet.add(item);
1678 * dataSet.add(data);
1679 * dataSet.update(item);
1680 * dataSet.update(data);
1681 * dataSet.remove(id);
1682 * dataSet.remove(ids);
1683 * var data = dataSet.get();
1684 * var data = dataSet.get(id);
1685 * var data = dataSet.get(ids);
1686 * var data = dataSet.get(ids, options, data);
1687 * dataSet.clear();
1688 *
1689 * A data set can:
1690 * - add/remove/update data
1691 * - gives triggers upon changes in the data
1692 * - can import/export data in various data formats
1693 *
1694 * @param {Array | DataTable} [data] Optional array with initial data
1695 * @param {Object} [options] Available options:
1696 * {String} fieldId Field name of the id in the
1697 * items, 'id' by default.
1698 * {Object.<String, String} type
1699 * A map with field names as key,
1700 * and the field type as value.
1701 * @constructor DataSet
1702 */
1703 // TODO: add a DataSet constructor DataSet(data, options)
1704 function DataSet (data, options) {
1705 // correctly read optional arguments
1706 if (data && !Array.isArray(data) && !util.isDataTable(data)) {
1707 options = data;
1708 data = null;
1709 }
1710
1711 this._options = options || {};
1712 this._data = {}; // map with data indexed by id
1713 this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
1714 this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
1715
1716 // all variants of a Date are internally stored as Date, so we can convert
1717 // from everything to everything (also from ISODate to Number for example)
1718 if (this._options.type) {
1719 for (var field in this._options.type) {
1720 if (this._options.type.hasOwnProperty(field)) {
1721 var value = this._options.type[field];
1722 if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
1723 this._type[field] = 'Date';
1724 }
1725 else {
1726 this._type[field] = value;
1727 }
1728 }
1729 }
1730 }
1731
1732 // TODO: deprecated since version 1.1.1 (or 2.0.0?)
1733 if (this._options.convert) {
1734 throw new Error('Option "convert" is deprecated. Use "type" instead.');
1735 }
1736
1737 this._subscribers = {}; // event subscribers
1738
1739 // add initial data when provided
1740 if (data) {
1741 this.add(data);
1742 }
1743 }
1744
1745 /**
1746 * Subscribe to an event, add an event listener
1747 * @param {String} event Event name. Available events: 'put', 'update',
1748 * 'remove'
1749 * @param {function} callback Callback method. Called with three parameters:
1750 * {String} event
1751 * {Object | null} params
1752 * {String | Number} senderId
1753 */
1754 DataSet.prototype.on = function(event, callback) {
1755 var subscribers = this._subscribers[event];
1756 if (!subscribers) {
1757 subscribers = [];
1758 this._subscribers[event] = subscribers;
1759 }
1760
1761 subscribers.push({
1762 callback: callback
1763 });
1764 };
1765
1766 // TODO: make this function deprecated (replaced with `on` since version 0.5)
1767 DataSet.prototype.subscribe = DataSet.prototype.on;
1768
1769 /**
1770 * Unsubscribe from an event, remove an event listener
1771 * @param {String} event
1772 * @param {function} callback
1773 */
1774 DataSet.prototype.off = function(event, callback) {
1775 var subscribers = this._subscribers[event];
1776 if (subscribers) {
1777 this._subscribers[event] = subscribers.filter(function (listener) {
1778 return (listener.callback != callback);
1779 });
1780 }
1781 };
1782
1783 // TODO: make this function deprecated (replaced with `on` since version 0.5)
1784 DataSet.prototype.unsubscribe = DataSet.prototype.off;
1785
1786 /**
1787 * Trigger an event
1788 * @param {String} event
1789 * @param {Object | null} params
1790 * @param {String} [senderId] Optional id of the sender.
1791 * @private
1792 */
1793 DataSet.prototype._trigger = function (event, params, senderId) {
1794 if (event == '*') {
1795 throw new Error('Cannot trigger event *');
1796 }
1797
1798 var subscribers = [];
1799 if (event in this._subscribers) {
1800 subscribers = subscribers.concat(this._subscribers[event]);
1801 }
1802 if ('*' in this._subscribers) {
1803 subscribers = subscribers.concat(this._subscribers['*']);
1804 }
1805
1806 for (var i = 0; i < subscribers.length; i++) {
1807 var subscriber = subscribers[i];
1808 if (subscriber.callback) {
1809 subscriber.callback(event, params, senderId || null);
1810 }
1811 }
1812 };
1813
1814 /**
1815 * Add data.
1816 * Adding an item will fail when there already is an item with the same id.
1817 * @param {Object | Array | DataTable} data
1818 * @param {String} [senderId] Optional sender id
1819 * @return {Array} addedIds Array with the ids of the added items
1820 */
1821 DataSet.prototype.add = function (data, senderId) {
1822 var addedIds = [],
1823 id,
1824 me = this;
1825
1826 if (Array.isArray(data)) {
1827 // Array
1828 for (var i = 0, len = data.length; i < len; i++) {
1829 id = me._addItem(data[i]);
1830 addedIds.push(id);
1831 }
1832 }
1833 else if (util.isDataTable(data)) {
1834 // Google DataTable
1835 var columns = this._getColumnNames(data);
1836 for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
1837 var item = {};
1838 for (var col = 0, cols = columns.length; col < cols; col++) {
1839 var field = columns[col];
1840 item[field] = data.getValue(row, col);
1841 }
1842
1843 id = me._addItem(item);
1844 addedIds.push(id);
1845 }
1846 }
1847 else if (data instanceof Object) {
1848 // Single item
1849 id = me._addItem(data);
1850 addedIds.push(id);
1851 }
1852 else {
1853 throw new Error('Unknown dataType');
1854 }
1855
1856 if (addedIds.length) {
1857 this._trigger('add', {items: addedIds}, senderId);
1858 }
1859
1860 return addedIds;
1861 };
1862
1863 /**
1864 * Update existing items. When an item does not exist, it will be created
1865 * @param {Object | Array | DataTable} data
1866 * @param {String} [senderId] Optional sender id
1867 * @return {Array} updatedIds The ids of the added or updated items
1868 */
1869 DataSet.prototype.update = function (data, senderId) {
1870 var addedIds = [],
1871 updatedIds = [],
1872 me = this,
1873 fieldId = me._fieldId;
1874
1875 var addOrUpdate = function (item) {
1876 var id = item[fieldId];
1877 if (me._data[id]) {
1878 // update item
1879 id = me._updateItem(item);
1880 updatedIds.push(id);
1881 }
1882 else {
1883 // add new item
1884 id = me._addItem(item);
1885 addedIds.push(id);
1886 }
1887 };
1888
1889 if (Array.isArray(data)) {
1890 // Array
1891 for (var i = 0, len = data.length; i < len; i++) {
1892 addOrUpdate(data[i]);
1893 }
1894 }
1895 else if (util.isDataTable(data)) {
1896 // Google DataTable
1897 var columns = this._getColumnNames(data);
1898 for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
1899 var item = {};
1900 for (var col = 0, cols = columns.length; col < cols; col++) {
1901 var field = columns[col];
1902 item[field] = data.getValue(row, col);
1903 }
1904
1905 addOrUpdate(item);
1906 }
1907 }
1908 else if (data instanceof Object) {
1909 // Single item
1910 addOrUpdate(data);
1911 }
1912 else {
1913 throw new Error('Unknown dataType');
1914 }
1915
1916 if (addedIds.length) {
1917 this._trigger('add', {items: addedIds}, senderId);
1918 }
1919 if (updatedIds.length) {
1920 this._trigger('update', {items: updatedIds}, senderId);
1921 }
1922
1923 return addedIds.concat(updatedIds);
1924 };
1925
1926 /**
1927 * Get a data item or multiple items.
1928 *
1929 * Usage:
1930 *
1931 * get()
1932 * get(options: Object)
1933 * get(options: Object, data: Array | DataTable)
1934 *
1935 * get(id: Number | String)
1936 * get(id: Number | String, options: Object)
1937 * get(id: Number | String, options: Object, data: Array | DataTable)
1938 *
1939 * get(ids: Number[] | String[])
1940 * get(ids: Number[] | String[], options: Object)
1941 * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
1942 *
1943 * Where:
1944 *
1945 * {Number | String} id The id of an item
1946 * {Number[] | String{}} ids An array with ids of items
1947 * {Object} options An Object with options. Available options:
1948 * {String} [returnType] Type of data to be
1949 * returned. Can be 'DataTable' or 'Array' (default)
1950 * {Object.<String, String>} [type]
1951 * {String[]} [fields] field names to be returned
1952 * {function} [filter] filter items
1953 * {String | function} [order] Order the items by
1954 * a field name or custom sort function.
1955 * {Array | DataTable} [data] If provided, items will be appended to this
1956 * array or table. Required in case of Google
1957 * DataTable.
1958 *
1959 * @throws Error
1960 */
1961 DataSet.prototype.get = function (args) {
1962 var me = this;
1963
1964 // parse the arguments
1965 var id, ids, options, data;
1966 var firstType = util.getType(arguments[0]);
1967 if (firstType == 'String' || firstType == 'Number') {
1968 // get(id [, options] [, data])
1969 id = arguments[0];
1970 options = arguments[1];
1971 data = arguments[2];
1972 }
1973 else if (firstType == 'Array') {
1974 // get(ids [, options] [, data])
1975 ids = arguments[0];
1976 options = arguments[1];
1977 data = arguments[2];
1978 }
1979 else {
1980 // get([, options] [, data])
1981 options = arguments[0];
1982 data = arguments[1];
1983 }
1984
1985 // determine the return type
1986 var returnType;
1987 if (options && options.returnType) {
1988 var allowedValues = ["DataTable", "Array", "Object"];
1989 returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType;
1990
1991 if (data && (returnType != util.getType(data))) {
1992 throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
1993 'does not correspond with specified options.type (' + options.type + ')');
1994 }
1995 if (returnType == 'DataTable' && !util.isDataTable(data)) {
1996 throw new Error('Parameter "data" must be a DataTable ' +
1997 'when options.type is "DataTable"');
1998 }
1999 }
2000 else if (data) {
2001 returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
2002 }
2003 else {
2004 returnType = 'Array';
2005 }
2006
2007 // build options
2008 var type = options && options.type || this._options.type;
2009 var filter = options && options.filter;
2010 var items = [], item, itemId, i, len;
2011
2012 // convert items
2013 if (id != undefined) {
2014 // return a single item
2015 item = me._getItem(id, type);
2016 if (filter && !filter(item)) {
2017 item = null;
2018 }
2019 }
2020 else if (ids != undefined) {
2021 // return a subset of items
2022 for (i = 0, len = ids.length; i < len; i++) {
2023 item = me._getItem(ids[i], type);
2024 if (!filter || filter(item)) {
2025 items.push(item);
2026 }
2027 }
2028 }
2029 else {
2030 // return all items
2031 for (itemId in this._data) {
2032 if (this._data.hasOwnProperty(itemId)) {
2033 item = me._getItem(itemId, type);
2034 if (!filter || filter(item)) {
2035 items.push(item);
2036 }
2037 }
2038 }
2039 }
2040
2041 // order the results
2042 if (options && options.order && id == undefined) {
2043 this._sort(items, options.order);
2044 }
2045
2046 // filter fields of the items
2047 if (options && options.fields) {
2048 var fields = options.fields;
2049 if (id != undefined) {
2050 item = this._filterFields(item, fields);
2051 }
2052 else {
2053 for (i = 0, len = items.length; i < len; i++) {
2054 items[i] = this._filterFields(items[i], fields);
2055 }
2056 }
2057 }
2058
2059 // return the results
2060 if (returnType == 'DataTable') {
2061 var columns = this._getColumnNames(data);
2062 if (id != undefined) {
2063 // append a single item to the data table
2064 me._appendRow(data, columns, item);
2065 }
2066 else {
2067 // copy the items to the provided data table
2068 for (i = 0; i < items.length; i++) {
2069 me._appendRow(data, columns, items[i]);
2070 }
2071 }
2072 return data;
2073 }
2074 else if (returnType == "Object") {
2075 var result = {};
2076 for (i = 0; i < items.length; i++) {
2077 result[items[i].id] = items[i];
2078 }
2079 return result;
2080 }
2081 else {
2082 // return an array
2083 if (id != undefined) {
2084 // a single item
2085 return item;
2086 }
2087 else {
2088 // multiple items
2089 if (data) {
2090 // copy the items to the provided array
2091 for (i = 0, len = items.length; i < len; i++) {
2092 data.push(items[i]);
2093 }
2094 return data;
2095 }
2096 else {
2097 // just return our array
2098 return items;
2099 }
2100 }
2101 }
2102 };
2103
2104 /**
2105 * Get ids of all items or from a filtered set of items.
2106 * @param {Object} [options] An Object with options. Available options:
2107 * {function} [filter] filter items
2108 * {String | function} [order] Order the items by
2109 * a field name or custom sort function.
2110 * @return {Array} ids
2111 */
2112 DataSet.prototype.getIds = function (options) {
2113 var data = this._data,
2114 filter = options && options.filter,
2115 order = options && options.order,
2116 type = options && options.type || this._options.type,
2117 i,
2118 len,
2119 id,
2120 item,
2121 items,
2122 ids = [];
2123
2124 if (filter) {
2125 // get filtered items
2126 if (order) {
2127 // create ordered list
2128 items = [];
2129 for (id in data) {
2130 if (data.hasOwnProperty(id)) {
2131 item = this._getItem(id, type);
2132 if (filter(item)) {
2133 items.push(item);
2134 }
2135 }
2136 }
2137
2138 this._sort(items, order);
2139
2140 for (i = 0, len = items.length; i < len; i++) {
2141 ids[i] = items[i][this._fieldId];
2142 }
2143 }
2144 else {
2145 // create unordered list
2146 for (id in data) {
2147 if (data.hasOwnProperty(id)) {
2148 item = this._getItem(id, type);
2149 if (filter(item)) {
2150 ids.push(item[this._fieldId]);
2151 }
2152 }
2153 }
2154 }
2155 }
2156 else {
2157 // get all items
2158 if (order) {
2159 // create an ordered list
2160 items = [];
2161 for (id in data) {
2162 if (data.hasOwnProperty(id)) {
2163 items.push(data[id]);
2164 }
2165 }
2166
2167 this._sort(items, order);
2168
2169 for (i = 0, len = items.length; i < len; i++) {
2170 ids[i] = items[i][this._fieldId];
2171 }
2172 }
2173 else {
2174 // create unordered list
2175 for (id in data) {
2176 if (data.hasOwnProperty(id)) {
2177 item = data[id];
2178 ids.push(item[this._fieldId]);
2179 }
2180 }
2181 }
2182 }
2183
2184 return ids;
2185 };
2186
2187 /**
2188 * Returns the DataSet itself. Is overwritten for example by the DataView,
2189 * which returns the DataSet it is connected to instead.
2190 */
2191 DataSet.prototype.getDataSet = function () {
2192 return this;
2193 };
2194
2195 /**
2196 * Execute a callback function for every item in the dataset.
2197 * @param {function} callback
2198 * @param {Object} [options] Available options:
2199 * {Object.<String, String>} [type]
2200 * {String[]} [fields] filter fields
2201 * {function} [filter] filter items
2202 * {String | function} [order] Order the items by
2203 * a field name or custom sort function.
2204 */
2205 DataSet.prototype.forEach = function (callback, options) {
2206 var filter = options && options.filter,
2207 type = options && options.type || this._options.type,
2208 data = this._data,
2209 item,
2210 id;
2211
2212 if (options && options.order) {
2213 // execute forEach on ordered list
2214 var items = this.get(options);
2215
2216 for (var i = 0, len = items.length; i < len; i++) {
2217 item = items[i];
2218 id = item[this._fieldId];
2219 callback(item, id);
2220 }
2221 }
2222 else {
2223 // unordered
2224 for (id in data) {
2225 if (data.hasOwnProperty(id)) {
2226 item = this._getItem(id, type);
2227 if (!filter || filter(item)) {
2228 callback(item, id);
2229 }
2230 }
2231 }
2232 }
2233 };
2234
2235 /**
2236 * Map every item in the dataset.
2237 * @param {function} callback
2238 * @param {Object} [options] Available options:
2239 * {Object.<String, String>} [type]
2240 * {String[]} [fields] filter fields
2241 * {function} [filter] filter items
2242 * {String | function} [order] Order the items by
2243 * a field name or custom sort function.
2244 * @return {Object[]} mappedItems
2245 */
2246 DataSet.prototype.map = function (callback, options) {
2247 var filter = options && options.filter,
2248 type = options && options.type || this._options.type,
2249 mappedItems = [],
2250 data = this._data,
2251 item;
2252
2253 // convert and filter items
2254 for (var id in data) {
2255 if (data.hasOwnProperty(id)) {
2256 item = this._getItem(id, type);
2257 if (!filter || filter(item)) {
2258 mappedItems.push(callback(item, id));
2259 }
2260 }
2261 }
2262
2263 // order items
2264 if (options && options.order) {
2265 this._sort(mappedItems, options.order);
2266 }
2267
2268 return mappedItems;
2269 };
2270
2271 /**
2272 * Filter the fields of an item
2273 * @param {Object} item
2274 * @param {String[]} fields Field names
2275 * @return {Object} filteredItem
2276 * @private
2277 */
2278 DataSet.prototype._filterFields = function (item, fields) {
2279 var filteredItem = {};
2280
2281 for (var field in item) {
2282 if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
2283 filteredItem[field] = item[field];
2284 }
2285 }
2286
2287 return filteredItem;
2288 };
2289
2290 /**
2291 * Sort the provided array with items
2292 * @param {Object[]} items
2293 * @param {String | function} order A field name or custom sort function.
2294 * @private
2295 */
2296 DataSet.prototype._sort = function (items, order) {
2297 if (util.isString(order)) {
2298 // order by provided field name
2299 var name = order; // field name
2300 items.sort(function (a, b) {
2301 var av = a[name];
2302 var bv = b[name];
2303 return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
2304 });
2305 }
2306 else if (typeof order === 'function') {
2307 // order by sort function
2308 items.sort(order);
2309 }
2310 // TODO: extend order by an Object {field:String, direction:String}
2311 // where direction can be 'asc' or 'desc'
2312 else {
2313 throw new TypeError('Order must be a function or a string');
2314 }
2315 };
2316
2317 /**
2318 * Remove an object by pointer or by id
2319 * @param {String | Number | Object | Array} id Object or id, or an array with
2320 * objects or ids to be removed
2321 * @param {String} [senderId] Optional sender id
2322 * @return {Array} removedIds
2323 */
2324 DataSet.prototype.remove = function (id, senderId) {
2325 var removedIds = [],
2326 i, len, removedId;
2327
2328 if (Array.isArray(id)) {
2329 for (i = 0, len = id.length; i < len; i++) {
2330 removedId = this._remove(id[i]);
2331 if (removedId != null) {
2332 removedIds.push(removedId);
2333 }
2334 }
2335 }
2336 else {
2337 removedId = this._remove(id);
2338 if (removedId != null) {
2339 removedIds.push(removedId);
2340 }
2341 }
2342
2343 if (removedIds.length) {
2344 this._trigger('remove', {items: removedIds}, senderId);
2345 }
2346
2347 return removedIds;
2348 };
2349
2350 /**
2351 * Remove an item by its id
2352 * @param {Number | String | Object} id id or item
2353 * @returns {Number | String | null} id
2354 * @private
2355 */
2356 DataSet.prototype._remove = function (id) {
2357 if (util.isNumber(id) || util.isString(id)) {
2358 if (this._data[id]) {
2359 delete this._data[id];
2360 return id;
2361 }
2362 }
2363 else if (id instanceof Object) {
2364 var itemId = id[this._fieldId];
2365 if (itemId && this._data[itemId]) {
2366 delete this._data[itemId];
2367 return itemId;
2368 }
2369 }
2370 return null;
2371 };
2372
2373 /**
2374 * Clear the data
2375 * @param {String} [senderId] Optional sender id
2376 * @return {Array} removedIds The ids of all removed items
2377 */
2378 DataSet.prototype.clear = function (senderId) {
2379 var ids = Object.keys(this._data);
2380
2381 this._data = {};
2382
2383 this._trigger('remove', {items: ids}, senderId);
2384
2385 return ids;
2386 };
2387
2388 /**
2389 * Find the item with maximum value of a specified field
2390 * @param {String} field
2391 * @return {Object | null} item Item containing max value, or null if no items
2392 */
2393 DataSet.prototype.max = function (field) {
2394 var data = this._data,
2395 max = null,
2396 maxField = null;
2397
2398 for (var id in data) {
2399 if (data.hasOwnProperty(id)) {
2400 var item = data[id];
2401 var itemField = item[field];
2402 if (itemField != null && (!max || itemField > maxField)) {
2403 max = item;
2404 maxField = itemField;
2405 }
2406 }
2407 }
2408
2409 return max;
2410 };
2411
2412 /**
2413 * Find the item with minimum value of a specified field
2414 * @param {String} field
2415 * @return {Object | null} item Item containing max value, or null if no items
2416 */
2417 DataSet.prototype.min = function (field) {
2418 var data = this._data,
2419 min = null,
2420 minField = null;
2421
2422 for (var id in data) {
2423 if (data.hasOwnProperty(id)) {
2424 var item = data[id];
2425 var itemField = item[field];
2426 if (itemField != null && (!min || itemField < minField)) {
2427 min = item;
2428 minField = itemField;
2429 }
2430 }
2431 }
2432
2433 return min;
2434 };
2435
2436 /**
2437 * Find all distinct values of a specified field
2438 * @param {String} field
2439 * @return {Array} values Array containing all distinct values. If data items
2440 * do not contain the specified field are ignored.
2441 * The returned array is unordered.
2442 */
2443 DataSet.prototype.distinct = function (field) {
2444 var data = this._data;
2445 var values = [];
2446 var fieldType = this._options.type && this._options.type[field] || null;
2447 var count = 0;
2448 var i;
2449
2450 for (var prop in data) {
2451 if (data.hasOwnProperty(prop)) {
2452 var item = data[prop];
2453 var value = item[field];
2454 var exists = false;
2455 for (i = 0; i < count; i++) {
2456 if (values[i] == value) {
2457 exists = true;
2458 break;
2459 }
2460 }
2461 if (!exists && (value !== undefined)) {
2462 values[count] = value;
2463 count++;
2464 }
2465 }
2466 }
2467
2468 if (fieldType) {
2469 for (i = 0; i < values.length; i++) {
2470 values[i] = util.convert(values[i], fieldType);
2471 }
2472 }
2473
2474 return values;
2475 };
2476
2477 /**
2478 * Add a single item. Will fail when an item with the same id already exists.
2479 * @param {Object} item
2480 * @return {String} id
2481 * @private
2482 */
2483 DataSet.prototype._addItem = function (item) {
2484 var id = item[this._fieldId];
2485
2486 if (id != undefined) {
2487 // check whether this id is already taken
2488 if (this._data[id]) {
2489 // item already exists
2490 throw new Error('Cannot add item: item with id ' + id + ' already exists');
2491 }
2492 }
2493 else {
2494 // generate an id
2495 id = util.randomUUID();
2496 item[this._fieldId] = id;
2497 }
2498
2499 var d = {};
2500 for (var field in item) {
2501 if (item.hasOwnProperty(field)) {
2502 var fieldType = this._type[field]; // type may be undefined
2503 d[field] = util.convert(item[field], fieldType);
2504 }
2505 }
2506 this._data[id] = d;
2507
2508 return id;
2509 };
2510
2511 /**
2512 * Get an item. Fields can be converted to a specific type
2513 * @param {String} id
2514 * @param {Object.<String, String>} [types] field types to convert
2515 * @return {Object | null} item
2516 * @private
2517 */
2518 DataSet.prototype._getItem = function (id, types) {
2519 var field, value;
2520
2521 // get the item from the dataset
2522 var raw = this._data[id];
2523 if (!raw) {
2524 return null;
2525 }
2526
2527 // convert the items field types
2528 var converted = {};
2529 if (types) {
2530 for (field in raw) {
2531 if (raw.hasOwnProperty(field)) {
2532 value = raw[field];
2533 converted[field] = util.convert(value, types[field]);
2534 }
2535 }
2536 }
2537 else {
2538 // no field types specified, no converting needed
2539 for (field in raw) {
2540 if (raw.hasOwnProperty(field)) {
2541 value = raw[field];
2542 converted[field] = value;
2543 }
2544 }
2545 }
2546 return converted;
2547 };
2548
2549 /**
2550 * Update a single item: merge with existing item.
2551 * Will fail when the item has no id, or when there does not exist an item
2552 * with the same id.
2553 * @param {Object} item
2554 * @return {String} id
2555 * @private
2556 */
2557 DataSet.prototype._updateItem = function (item) {
2558 var id = item[this._fieldId];
2559 if (id == undefined) {
2560 throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
2561 }
2562 var d = this._data[id];
2563 if (!d) {
2564 // item doesn't exist
2565 throw new Error('Cannot update item: no item with id ' + id + ' found');
2566 }
2567
2568 // merge with current item
2569 for (var field in item) {
2570 if (item.hasOwnProperty(field)) {
2571 var fieldType = this._type[field]; // type may be undefined
2572 d[field] = util.convert(item[field], fieldType);
2573 }
2574 }
2575
2576 return id;
2577 };
2578
2579 /**
2580 * Get an array with the column names of a Google DataTable
2581 * @param {DataTable} dataTable
2582 * @return {String[]} columnNames
2583 * @private
2584 */
2585 DataSet.prototype._getColumnNames = function (dataTable) {
2586 var columns = [];
2587 for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
2588 columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
2589 }
2590 return columns;
2591 };
2592
2593 /**
2594 * Append an item as a row to the dataTable
2595 * @param dataTable
2596 * @param columns
2597 * @param item
2598 * @private
2599 */
2600 DataSet.prototype._appendRow = function (dataTable, columns, item) {
2601 var row = dataTable.addRow();
2602
2603 for (var col = 0, cols = columns.length; col < cols; col++) {
2604 var field = columns[col];
2605 dataTable.setValue(row, col, item[field]);
2606 }
2607 };
2608
2609 module.exports = DataSet;
2610
2611
2612/***/ },
2613/* 4 */
2614/***/ function(module, exports, __webpack_require__) {
2615
2616 var util = __webpack_require__(1);
2617 var DataSet = __webpack_require__(3);
2618
2619 /**
2620 * DataView
2621 *
2622 * a dataview offers a filtered view on a dataset or an other dataview.
2623 *
2624 * @param {DataSet | DataView} data
2625 * @param {Object} [options] Available options: see method get
2626 *
2627 * @constructor DataView
2628 */
2629 function DataView (data, options) {
2630 this._data = null;
2631 this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
2632 this._options = options || {};
2633 this._fieldId = 'id'; // name of the field containing id
2634 this._subscribers = {}; // event subscribers
2635
2636 var me = this;
2637 this.listener = function () {
2638 me._onEvent.apply(me, arguments);
2639 };
2640
2641 this.setData(data);
2642 }
2643
2644 // TODO: implement a function .config() to dynamically update things like configured filter
2645 // and trigger changes accordingly
2646
2647 /**
2648 * Set a data source for the view
2649 * @param {DataSet | DataView} data
2650 */
2651 DataView.prototype.setData = function (data) {
2652 var ids, i, len;
2653
2654 if (this._data) {
2655 // unsubscribe from current dataset
2656 if (this._data.unsubscribe) {
2657 this._data.unsubscribe('*', this.listener);
2658 }
2659
2660 // trigger a remove of all items in memory
2661 ids = [];
2662 for (var id in this._ids) {
2663 if (this._ids.hasOwnProperty(id)) {
2664 ids.push(id);
2665 }
2666 }
2667 this._ids = {};
2668 this._trigger('remove', {items: ids});
2669 }
2670
2671 this._data = data;
2672
2673 if (this._data) {
2674 // update fieldId
2675 this._fieldId = this._options.fieldId ||
2676 (this._data && this._data.options && this._data.options.fieldId) ||
2677 'id';
2678
2679 // trigger an add of all added items
2680 ids = this._data.getIds({filter: this._options && this._options.filter});
2681 for (i = 0, len = ids.length; i < len; i++) {
2682 id = ids[i];
2683 this._ids[id] = true;
2684 }
2685 this._trigger('add', {items: ids});
2686
2687 // subscribe to new dataset
2688 if (this._data.on) {
2689 this._data.on('*', this.listener);
2690 }
2691 }
2692 };
2693
2694 /**
2695 * Get data from the data view
2696 *
2697 * Usage:
2698 *
2699 * get()
2700 * get(options: Object)
2701 * get(options: Object, data: Array | DataTable)
2702 *
2703 * get(id: Number)
2704 * get(id: Number, options: Object)
2705 * get(id: Number, options: Object, data: Array | DataTable)
2706 *
2707 * get(ids: Number[])
2708 * get(ids: Number[], options: Object)
2709 * get(ids: Number[], options: Object, data: Array | DataTable)
2710 *
2711 * Where:
2712 *
2713 * {Number | String} id The id of an item
2714 * {Number[] | String{}} ids An array with ids of items
2715 * {Object} options An Object with options. Available options:
2716 * {String} [type] Type of data to be returned. Can
2717 * be 'DataTable' or 'Array' (default)
2718 * {Object.<String, String>} [convert]
2719 * {String[]} [fields] field names to be returned
2720 * {function} [filter] filter items
2721 * {String | function} [order] Order the items by
2722 * a field name or custom sort function.
2723 * {Array | DataTable} [data] If provided, items will be appended to this
2724 * array or table. Required in case of Google
2725 * DataTable.
2726 * @param args
2727 */
2728 DataView.prototype.get = function (args) {
2729 var me = this;
2730
2731 // parse the arguments
2732 var ids, options, data;
2733 var firstType = util.getType(arguments[0]);
2734 if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
2735 // get(id(s) [, options] [, data])
2736 ids = arguments[0]; // can be a single id or an array with ids
2737 options = arguments[1];
2738 data = arguments[2];
2739 }
2740 else {
2741 // get([, options] [, data])
2742 options = arguments[0];
2743 data = arguments[1];
2744 }
2745
2746 // extend the options with the default options and provided options
2747 var viewOptions = util.extend({}, this._options, options);
2748
2749 // create a combined filter method when needed
2750 if (this._options.filter && options && options.filter) {
2751 viewOptions.filter = function (item) {
2752 return me._options.filter(item) && options.filter(item);
2753 }
2754 }
2755
2756 // build up the call to the linked data set
2757 var getArguments = [];
2758 if (ids != undefined) {
2759 getArguments.push(ids);
2760 }
2761 getArguments.push(viewOptions);
2762 getArguments.push(data);
2763
2764 return this._data && this._data.get.apply(this._data, getArguments);
2765 };
2766
2767 /**
2768 * Get ids of all items or from a filtered set of items.
2769 * @param {Object} [options] An Object with options. Available options:
2770 * {function} [filter] filter items
2771 * {String | function} [order] Order the items by
2772 * a field name or custom sort function.
2773 * @return {Array} ids
2774 */
2775 DataView.prototype.getIds = function (options) {
2776 var ids;
2777
2778 if (this._data) {
2779 var defaultFilter = this._options.filter;
2780 var filter;
2781
2782 if (options && options.filter) {
2783 if (defaultFilter) {
2784 filter = function (item) {
2785 return defaultFilter(item) && options.filter(item);
2786 }
2787 }
2788 else {
2789 filter = options.filter;
2790 }
2791 }
2792 else {
2793 filter = defaultFilter;
2794 }
2795
2796 ids = this._data.getIds({
2797 filter: filter,
2798 order: options && options.order
2799 });
2800 }
2801 else {
2802 ids = [];
2803 }
2804
2805 return ids;
2806 };
2807
2808 /**
2809 * Get the DataSet to which this DataView is connected. In case there is a chain
2810 * of multiple DataViews, the root DataSet of this chain is returned.
2811 * @return {DataSet} dataSet
2812 */
2813 DataView.prototype.getDataSet = function () {
2814 var dataSet = this;
2815 while (dataSet instanceof DataView) {
2816 dataSet = dataSet._data;
2817 }
2818 return dataSet || null;
2819 };
2820
2821 /**
2822 * Event listener. Will propagate all events from the connected data set to
2823 * the subscribers of the DataView, but will filter the items and only trigger
2824 * when there are changes in the filtered data set.
2825 * @param {String} event
2826 * @param {Object | null} params
2827 * @param {String} senderId
2828 * @private
2829 */
2830 DataView.prototype._onEvent = function (event, params, senderId) {
2831 var i, len, id, item,
2832 ids = params && params.items,
2833 data = this._data,
2834 added = [],
2835 updated = [],
2836 removed = [];
2837
2838 if (ids && data) {
2839 switch (event) {
2840 case 'add':
2841 // filter the ids of the added items
2842 for (i = 0, len = ids.length; i < len; i++) {
2843 id = ids[i];
2844 item = this.get(id);
2845 if (item) {
2846 this._ids[id] = true;
2847 added.push(id);
2848 }
2849 }
2850
2851 break;
2852
2853 case 'update':
2854 // determine the event from the views viewpoint: an updated
2855 // item can be added, updated, or removed from this view.
2856 for (i = 0, len = ids.length; i < len; i++) {
2857 id = ids[i];
2858 item = this.get(id);
2859
2860 if (item) {
2861 if (this._ids[id]) {
2862 updated.push(id);
2863 }
2864 else {
2865 this._ids[id] = true;
2866 added.push(id);
2867 }
2868 }
2869 else {
2870 if (this._ids[id]) {
2871 delete this._ids[id];
2872 removed.push(id);
2873 }
2874 else {
2875 // nothing interesting for me :-(
2876 }
2877 }
2878 }
2879
2880 break;
2881
2882 case 'remove':
2883 // filter the ids of the removed items
2884 for (i = 0, len = ids.length; i < len; i++) {
2885 id = ids[i];
2886 if (this._ids[id]) {
2887 delete this._ids[id];
2888 removed.push(id);
2889 }
2890 }
2891
2892 break;
2893 }
2894
2895 if (added.length) {
2896 this._trigger('add', {items: added}, senderId);
2897 }
2898 if (updated.length) {
2899 this._trigger('update', {items: updated}, senderId);
2900 }
2901 if (removed.length) {
2902 this._trigger('remove', {items: removed}, senderId);
2903 }
2904 }
2905 };
2906
2907 // copy subscription functionality from DataSet
2908 DataView.prototype.on = DataSet.prototype.on;
2909 DataView.prototype.off = DataSet.prototype.off;
2910 DataView.prototype._trigger = DataSet.prototype._trigger;
2911
2912 // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
2913 DataView.prototype.subscribe = DataView.prototype.on;
2914 DataView.prototype.unsubscribe = DataView.prototype.off;
2915
2916 module.exports = DataView;
2917
2918/***/ },
2919/* 5 */
2920/***/ function(module, exports, __webpack_require__) {
2921
2922 var Emitter = __webpack_require__(50);
2923 var DataSet = __webpack_require__(3);
2924 var DataView = __webpack_require__(4);
2925 var util = __webpack_require__(1);
2926 var Point3d = __webpack_require__(9);
2927 var Point2d = __webpack_require__(8);
2928 var Camera = __webpack_require__(6);
2929 var Filter = __webpack_require__(7);
2930 var Slider = __webpack_require__(10);
2931 var StepNumber = __webpack_require__(11);
2932
2933 /**
2934 * @constructor Graph3d
2935 * Graph3d displays data in 3d.
2936 *
2937 * Graph3d is developed in javascript as a Google Visualization Chart.
2938 *
2939 * @param {Element} container The DOM element in which the Graph3d will
2940 * be created. Normally a div element.
2941 * @param {DataSet | DataView | Array} [data]
2942 * @param {Object} [options]
2943 */
2944 function Graph3d(container, data, options) {
2945 if (!(this instanceof Graph3d)) {
2946 throw new SyntaxError('Constructor must be called with the new operator');
2947 }
2948
2949 // create variables and set default values
2950 this.containerElement = container;
2951 this.width = '400px';
2952 this.height = '400px';
2953 this.margin = 10; // px
2954 this.defaultXCenter = '55%';
2955 this.defaultYCenter = '50%';
2956
2957 this.xLabel = 'x';
2958 this.yLabel = 'y';
2959 this.zLabel = 'z';
2960 this.filterLabel = 'time';
2961 this.legendLabel = 'value';
2962
2963 this.style = Graph3d.STYLE.DOT;
2964 this.showPerspective = true;
2965 this.showGrid = true;
2966 this.keepAspectRatio = true;
2967 this.showShadow = false;
2968 this.showGrayBottom = false; // TODO: this does not work correctly
2969 this.showTooltip = false;
2970 this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
2971
2972 this.animationInterval = 1000; // milliseconds
2973 this.animationPreload = false;
2974
2975 this.camera = new Camera();
2976 this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
2977
2978 this.dataTable = null; // The original data table
2979 this.dataPoints = null; // The table with point objects
2980
2981 // the column indexes
2982 this.colX = undefined;
2983 this.colY = undefined;
2984 this.colZ = undefined;
2985 this.colValue = undefined;
2986 this.colFilter = undefined;
2987
2988 this.xMin = 0;
2989 this.xStep = undefined; // auto by default
2990 this.xMax = 1;
2991 this.yMin = 0;
2992 this.yStep = undefined; // auto by default
2993 this.yMax = 1;
2994 this.zMin = 0;
2995 this.zStep = undefined; // auto by default
2996 this.zMax = 1;
2997 this.valueMin = 0;
2998 this.valueMax = 1;
2999 this.xBarWidth = 1;
3000 this.yBarWidth = 1;
3001 // TODO: customize axis range
3002
3003 // constants
3004 this.colorAxis = '#4D4D4D';
3005 this.colorGrid = '#D3D3D3';
3006 this.colorDot = '#7DC1FF';
3007 this.colorDotBorder = '#3267D2';
3008
3009 // create a frame and canvas
3010 this.create();
3011
3012 // apply options (also when undefined)
3013 this.setOptions(options);
3014
3015 // apply data
3016 if (data) {
3017 this.setData(data);
3018 }
3019 }
3020
3021 // Extend Graph3d with an Emitter mixin
3022 Emitter(Graph3d.prototype);
3023
3024 /**
3025 * Calculate the scaling values, dependent on the range in x, y, and z direction
3026 */
3027 Graph3d.prototype._setScale = function() {
3028 this.scale = new Point3d(1 / (this.xMax - this.xMin),
3029 1 / (this.yMax - this.yMin),
3030 1 / (this.zMax - this.zMin));
3031
3032 // keep aspect ration between x and y scale if desired
3033 if (this.keepAspectRatio) {
3034 if (this.scale.x < this.scale.y) {
3035 //noinspection JSSuspiciousNameCombination
3036 this.scale.y = this.scale.x;
3037 }
3038 else {
3039 //noinspection JSSuspiciousNameCombination
3040 this.scale.x = this.scale.y;
3041 }
3042 }
3043
3044 // scale the vertical axis
3045 this.scale.z *= this.verticalRatio;
3046 // TODO: can this be automated? verticalRatio?
3047
3048 // determine scale for (optional) value
3049 this.scale.value = 1 / (this.valueMax - this.valueMin);
3050
3051 // position the camera arm
3052 var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
3053 var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
3054 var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
3055 this.camera.setArmLocation(xCenter, yCenter, zCenter);
3056 };
3057
3058
3059 /**
3060 * Convert a 3D location to a 2D location on screen
3061 * http://en.wikipedia.org/wiki/3D_projection
3062 * @param {Point3d} point3d A 3D point with parameters x, y, z
3063 * @return {Point2d} point2d A 2D point with parameters x, y
3064 */
3065 Graph3d.prototype._convert3Dto2D = function(point3d) {
3066 var translation = this._convertPointToTranslation(point3d);
3067 return this._convertTranslationToScreen(translation);
3068 };
3069
3070 /**
3071 * Convert a 3D location its translation seen from the camera
3072 * http://en.wikipedia.org/wiki/3D_projection
3073 * @param {Point3d} point3d A 3D point with parameters x, y, z
3074 * @return {Point3d} translation A 3D point with parameters x, y, z This is
3075 * the translation of the point, seen from the
3076 * camera
3077 */
3078 Graph3d.prototype._convertPointToTranslation = function(point3d) {
3079 var ax = point3d.x * this.scale.x,
3080 ay = point3d.y * this.scale.y,
3081 az = point3d.z * this.scale.z,
3082
3083 cx = this.camera.getCameraLocation().x,
3084 cy = this.camera.getCameraLocation().y,
3085 cz = this.camera.getCameraLocation().z,
3086
3087 // calculate angles
3088 sinTx = Math.sin(this.camera.getCameraRotation().x),
3089 cosTx = Math.cos(this.camera.getCameraRotation().x),
3090 sinTy = Math.sin(this.camera.getCameraRotation().y),
3091 cosTy = Math.cos(this.camera.getCameraRotation().y),
3092 sinTz = Math.sin(this.camera.getCameraRotation().z),
3093 cosTz = Math.cos(this.camera.getCameraRotation().z),
3094
3095 // calculate translation
3096 dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
3097 dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
3098 dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
3099
3100 return new Point3d(dx, dy, dz);
3101 };
3102
3103 /**
3104 * Convert a translation point to a point on the screen
3105 * @param {Point3d} translation A 3D point with parameters x, y, z This is
3106 * the translation of the point, seen from the
3107 * camera
3108 * @return {Point2d} point2d A 2D point with parameters x, y
3109 */
3110 Graph3d.prototype._convertTranslationToScreen = function(translation) {
3111 var ex = this.eye.x,
3112 ey = this.eye.y,
3113 ez = this.eye.z,
3114 dx = translation.x,
3115 dy = translation.y,
3116 dz = translation.z;
3117
3118 // calculate position on screen from translation
3119 var bx;
3120 var by;
3121 if (this.showPerspective) {
3122 bx = (dx - ex) * (ez / dz);
3123 by = (dy - ey) * (ez / dz);
3124 }
3125 else {
3126 bx = dx * -(ez / this.camera.getArmLength());
3127 by = dy * -(ez / this.camera.getArmLength());
3128 }
3129
3130 // shift and scale the point to the center of the screen
3131 // use the width of the graph to scale both horizontally and vertically.
3132 return new Point2d(
3133 this.xcenter + bx * this.frame.canvas.clientWidth,
3134 this.ycenter - by * this.frame.canvas.clientWidth);
3135 };
3136
3137 /**
3138 * Set the background styling for the graph
3139 * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
3140 */
3141 Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
3142 var fill = 'white';
3143 var stroke = 'gray';
3144 var strokeWidth = 1;
3145
3146 if (typeof(backgroundColor) === 'string') {
3147 fill = backgroundColor;
3148 stroke = 'none';
3149 strokeWidth = 0;
3150 }
3151 else if (typeof(backgroundColor) === 'object') {
3152 if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
3153 if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
3154 if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
3155 }
3156 else if (backgroundColor === undefined) {
3157 // use use defaults
3158 }
3159 else {
3160 throw 'Unsupported type of backgroundColor';
3161 }
3162
3163 this.frame.style.backgroundColor = fill;
3164 this.frame.style.borderColor = stroke;
3165 this.frame.style.borderWidth = strokeWidth + 'px';
3166 this.frame.style.borderStyle = 'solid';
3167 };
3168
3169
3170 /// enumerate the available styles
3171 Graph3d.STYLE = {
3172 BAR: 0,
3173 BARCOLOR: 1,
3174 BARSIZE: 2,
3175 DOT : 3,
3176 DOTLINE : 4,
3177 DOTCOLOR: 5,
3178 DOTSIZE: 6,
3179 GRID : 7,
3180 LINE: 8,
3181 SURFACE : 9
3182 };
3183
3184 /**
3185 * Retrieve the style index from given styleName
3186 * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
3187 * @return {Number} styleNumber Enumeration value representing the style, or -1
3188 * when not found
3189 */
3190 Graph3d.prototype._getStyleNumber = function(styleName) {
3191 switch (styleName) {
3192 case 'dot': return Graph3d.STYLE.DOT;
3193 case 'dot-line': return Graph3d.STYLE.DOTLINE;
3194 case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
3195 case 'dot-size': return Graph3d.STYLE.DOTSIZE;
3196 case 'line': return Graph3d.STYLE.LINE;
3197 case 'grid': return Graph3d.STYLE.GRID;
3198 case 'surface': return Graph3d.STYLE.SURFACE;
3199 case 'bar': return Graph3d.STYLE.BAR;
3200 case 'bar-color': return Graph3d.STYLE.BARCOLOR;
3201 case 'bar-size': return Graph3d.STYLE.BARSIZE;
3202 }
3203
3204 return -1;
3205 };
3206
3207 /**
3208 * Determine the indexes of the data columns, based on the given style and data
3209 * @param {DataSet} data
3210 * @param {Number} style
3211 */
3212 Graph3d.prototype._determineColumnIndexes = function(data, style) {
3213 if (this.style === Graph3d.STYLE.DOT ||
3214 this.style === Graph3d.STYLE.DOTLINE ||
3215 this.style === Graph3d.STYLE.LINE ||
3216 this.style === Graph3d.STYLE.GRID ||
3217 this.style === Graph3d.STYLE.SURFACE ||
3218 this.style === Graph3d.STYLE.BAR) {
3219 // 3 columns expected, and optionally a 4th with filter values
3220 this.colX = 0;
3221 this.colY = 1;
3222 this.colZ = 2;
3223 this.colValue = undefined;
3224
3225 if (data.getNumberOfColumns() > 3) {
3226 this.colFilter = 3;
3227 }
3228 }
3229 else if (this.style === Graph3d.STYLE.DOTCOLOR ||
3230 this.style === Graph3d.STYLE.DOTSIZE ||
3231 this.style === Graph3d.STYLE.BARCOLOR ||
3232 this.style === Graph3d.STYLE.BARSIZE) {
3233 // 4 columns expected, and optionally a 5th with filter values
3234 this.colX = 0;
3235 this.colY = 1;
3236 this.colZ = 2;
3237 this.colValue = 3;
3238
3239 if (data.getNumberOfColumns() > 4) {
3240 this.colFilter = 4;
3241 }
3242 }
3243 else {
3244 throw 'Unknown style "' + this.style + '"';
3245 }
3246 };
3247
3248 Graph3d.prototype.getNumberOfRows = function(data) {
3249 return data.length;
3250 }
3251
3252
3253 Graph3d.prototype.getNumberOfColumns = function(data) {
3254 var counter = 0;
3255 for (var column in data[0]) {
3256 if (data[0].hasOwnProperty(column)) {
3257 counter++;
3258 }
3259 }
3260 return counter;
3261 }
3262
3263
3264 Graph3d.prototype.getDistinctValues = function(data, column) {
3265 var distinctValues = [];
3266 for (var i = 0; i < data.length; i++) {
3267 if (distinctValues.indexOf(data[i][column]) == -1) {
3268 distinctValues.push(data[i][column]);
3269 }
3270 }
3271 return distinctValues;
3272 }
3273
3274
3275 Graph3d.prototype.getColumnRange = function(data,column) {
3276 var minMax = {min:data[0][column],max:data[0][column]};
3277 for (var i = 0; i < data.length; i++) {
3278 if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
3279 if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
3280 }
3281 return minMax;
3282 };
3283
3284 /**
3285 * Initialize the data from the data table. Calculate minimum and maximum values
3286 * and column index values
3287 * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
3288 * @param {Number} style Style Number
3289 */
3290 Graph3d.prototype._dataInitialize = function (rawData, style) {
3291 var me = this;
3292
3293 // unsubscribe from the dataTable
3294 if (this.dataSet) {
3295 this.dataSet.off('*', this._onChange);
3296 }
3297
3298 if (rawData === undefined)
3299 return;
3300
3301 if (Array.isArray(rawData)) {
3302 rawData = new DataSet(rawData);
3303 }
3304
3305 var data;
3306 if (rawData instanceof DataSet || rawData instanceof DataView) {
3307 data = rawData.get();
3308 }
3309 else {
3310 throw new Error('Array, DataSet, or DataView expected');
3311 }
3312
3313 if (data.length == 0)
3314 return;
3315
3316 this.dataSet = rawData;
3317 this.dataTable = data;
3318
3319 // subscribe to changes in the dataset
3320 this._onChange = function () {
3321 me.setData(me.dataSet);
3322 };
3323 this.dataSet.on('*', this._onChange);
3324
3325 // _determineColumnIndexes
3326 // getNumberOfRows (points)
3327 // getNumberOfColumns (x,y,z,v,t,t1,t2...)
3328 // getDistinctValues (unique values?)
3329 // getColumnRange
3330
3331 // determine the location of x,y,z,value,filter columns
3332 this.colX = 'x';
3333 this.colY = 'y';
3334 this.colZ = 'z';
3335 this.colValue = 'style';
3336 this.colFilter = 'filter';
3337
3338
3339
3340 // check if a filter column is provided
3341 if (data[0].hasOwnProperty('filter')) {
3342 if (this.dataFilter === undefined) {
3343 this.dataFilter = new Filter(rawData, this.colFilter, this);
3344 this.dataFilter.setOnLoadCallback(function() {me.redraw();});
3345 }
3346 }
3347
3348
3349 var withBars = this.style == Graph3d.STYLE.BAR ||
3350 this.style == Graph3d.STYLE.BARCOLOR ||
3351 this.style == Graph3d.STYLE.BARSIZE;
3352
3353 // determine barWidth from data
3354 if (withBars) {
3355 if (this.defaultXBarWidth !== undefined) {
3356 this.xBarWidth = this.defaultXBarWidth;
3357 }
3358 else {
3359 var dataX = this.getDistinctValues(data,this.colX);
3360 this.xBarWidth = (dataX[1] - dataX[0]) || 1;
3361 }
3362
3363 if (this.defaultYBarWidth !== undefined) {
3364 this.yBarWidth = this.defaultYBarWidth;
3365 }
3366 else {
3367 var dataY = this.getDistinctValues(data,this.colY);
3368 this.yBarWidth = (dataY[1] - dataY[0]) || 1;
3369 }
3370 }
3371
3372 // calculate minimums and maximums
3373 var xRange = this.getColumnRange(data,this.colX);
3374 if (withBars) {
3375 xRange.min -= this.xBarWidth / 2;
3376 xRange.max += this.xBarWidth / 2;
3377 }
3378 this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
3379 this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
3380 if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
3381 this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
3382
3383 var yRange = this.getColumnRange(data,this.colY);
3384 if (withBars) {
3385 yRange.min -= this.yBarWidth / 2;
3386 yRange.max += this.yBarWidth / 2;
3387 }
3388 this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
3389 this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
3390 if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
3391 this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
3392
3393 var zRange = this.getColumnRange(data,this.colZ);
3394 this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
3395 this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
3396 if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
3397 this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
3398
3399 if (this.colValue !== undefined) {
3400 var valueRange = this.getColumnRange(data,this.colValue);
3401 this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
3402 this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
3403 if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
3404 }
3405
3406 // set the scale dependent on the ranges.
3407 this._setScale();
3408 };
3409
3410
3411
3412 /**
3413 * Filter the data based on the current filter
3414 * @param {Array} data
3415 * @return {Array} dataPoints Array with point objects which can be drawn on screen
3416 */
3417 Graph3d.prototype._getDataPoints = function (data) {
3418 // TODO: store the created matrix dataPoints in the filters instead of reloading each time
3419 var x, y, i, z, obj, point;
3420
3421 var dataPoints = [];
3422
3423 if (this.style === Graph3d.STYLE.GRID ||
3424 this.style === Graph3d.STYLE.SURFACE) {
3425 // copy all values from the google data table to a matrix
3426 // the provided values are supposed to form a grid of (x,y) positions
3427
3428 // create two lists with all present x and y values
3429 var dataX = [];
3430 var dataY = [];
3431 for (i = 0; i < this.getNumberOfRows(data); i++) {
3432 x = data[i][this.colX] || 0;
3433 y = data[i][this.colY] || 0;
3434
3435 if (dataX.indexOf(x) === -1) {
3436 dataX.push(x);
3437 }
3438 if (dataY.indexOf(y) === -1) {
3439 dataY.push(y);
3440 }
3441 }
3442
3443 function sortNumber(a, b) {
3444 return a - b;
3445 }
3446 dataX.sort(sortNumber);
3447 dataY.sort(sortNumber);
3448
3449 // create a grid, a 2d matrix, with all values.
3450 var dataMatrix = []; // temporary data matrix
3451 for (i = 0; i < data.length; i++) {
3452 x = data[i][this.colX] || 0;
3453 y = data[i][this.colY] || 0;
3454 z = data[i][this.colZ] || 0;
3455
3456 var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
3457 var yIndex = dataY.indexOf(y);
3458
3459 if (dataMatrix[xIndex] === undefined) {
3460 dataMatrix[xIndex] = [];
3461 }
3462
3463 var point3d = new Point3d();
3464 point3d.x = x;
3465 point3d.y = y;
3466 point3d.z = z;
3467
3468 obj = {};
3469 obj.point = point3d;
3470 obj.trans = undefined;
3471 obj.screen = undefined;
3472 obj.bottom = new Point3d(x, y, this.zMin);
3473
3474 dataMatrix[xIndex][yIndex] = obj;
3475
3476 dataPoints.push(obj);
3477 }
3478
3479 // fill in the pointers to the neighbors.
3480 for (x = 0; x < dataMatrix.length; x++) {
3481 for (y = 0; y < dataMatrix[x].length; y++) {
3482 if (dataMatrix[x][y]) {
3483 dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
3484 dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
3485 dataMatrix[x][y].pointCross =
3486 (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
3487 dataMatrix[x+1][y+1] :
3488 undefined;
3489 }
3490 }
3491 }
3492 }
3493 else { // 'dot', 'dot-line', etc.
3494 // copy all values from the google data table to a list with Point3d objects
3495 for (i = 0; i < data.length; i++) {
3496 point = new Point3d();
3497 point.x = data[i][this.colX] || 0;
3498 point.y = data[i][this.colY] || 0;
3499 point.z = data[i][this.colZ] || 0;
3500
3501 if (this.colValue !== undefined) {
3502 point.value = data[i][this.colValue] || 0;
3503 }
3504
3505 obj = {};
3506 obj.point = point;
3507 obj.bottom = new Point3d(point.x, point.y, this.zMin);
3508 obj.trans = undefined;
3509 obj.screen = undefined;
3510
3511 dataPoints.push(obj);
3512 }
3513 }
3514
3515 return dataPoints;
3516 };
3517
3518 /**
3519 * Create the main frame for the Graph3d.
3520 * This function is executed once when a Graph3d object is created. The frame
3521 * contains a canvas, and this canvas contains all objects like the axis and
3522 * nodes.
3523 */
3524 Graph3d.prototype.create = function () {
3525 // remove all elements from the container element.
3526 while (this.containerElement.hasChildNodes()) {
3527 this.containerElement.removeChild(this.containerElement.firstChild);
3528 }
3529
3530 this.frame = document.createElement('div');
3531 this.frame.style.position = 'relative';
3532 this.frame.style.overflow = 'hidden';
3533
3534 // create the graph canvas (HTML canvas element)
3535 this.frame.canvas = document.createElement( 'canvas' );
3536 this.frame.canvas.style.position = 'relative';
3537 this.frame.appendChild(this.frame.canvas);
3538 //if (!this.frame.canvas.getContext) {
3539 {
3540 var noCanvas = document.createElement( 'DIV' );
3541 noCanvas.style.color = 'red';
3542 noCanvas.style.fontWeight = 'bold' ;
3543 noCanvas.style.padding = '10px';
3544 noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
3545 this.frame.canvas.appendChild(noCanvas);
3546 }
3547
3548 this.frame.filter = document.createElement( 'div' );
3549 this.frame.filter.style.position = 'absolute';
3550 this.frame.filter.style.bottom = '0px';
3551 this.frame.filter.style.left = '0px';
3552 this.frame.filter.style.width = '100%';
3553 this.frame.appendChild(this.frame.filter);
3554
3555 // add event listeners to handle moving and zooming the contents
3556 var me = this;
3557 var onmousedown = function (event) {me._onMouseDown(event);};
3558 var ontouchstart = function (event) {me._onTouchStart(event);};
3559 var onmousewheel = function (event) {me._onWheel(event);};
3560 var ontooltip = function (event) {me._onTooltip(event);};
3561 // TODO: these events are never cleaned up... can give a 'memory leakage'
3562
3563 util.addEventListener(this.frame.canvas, 'keydown', onkeydown);
3564 util.addEventListener(this.frame.canvas, 'mousedown', onmousedown);
3565 util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart);
3566 util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
3567 util.addEventListener(this.frame.canvas, 'mousemove', ontooltip);
3568
3569 // add the new graph to the container element
3570 this.containerElement.appendChild(this.frame);
3571 };
3572
3573
3574 /**
3575 * Set a new size for the graph
3576 * @param {string} width Width in pixels or percentage (for example '800px'
3577 * or '50%')
3578 * @param {string} height Height in pixels or percentage (for example '400px'
3579 * or '30%')
3580 */
3581 Graph3d.prototype.setSize = function(width, height) {
3582 this.frame.style.width = width;
3583 this.frame.style.height = height;
3584
3585 this._resizeCanvas();
3586 };
3587
3588 /**
3589 * Resize the canvas to the current size of the frame
3590 */
3591 Graph3d.prototype._resizeCanvas = function() {
3592 this.frame.canvas.style.width = '100%';
3593 this.frame.canvas.style.height = '100%';
3594
3595 this.frame.canvas.width = this.frame.canvas.clientWidth;
3596 this.frame.canvas.height = this.frame.canvas.clientHeight;
3597
3598 // adjust with for margin
3599 this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
3600 };
3601
3602 /**
3603 * Start animation
3604 */
3605 Graph3d.prototype.animationStart = function() {
3606 if (!this.frame.filter || !this.frame.filter.slider)
3607 throw 'No animation available';
3608
3609 this.frame.filter.slider.play();
3610 };
3611
3612
3613 /**
3614 * Stop animation
3615 */
3616 Graph3d.prototype.animationStop = function() {
3617 if (!this.frame.filter || !this.frame.filter.slider) return;
3618
3619 this.frame.filter.slider.stop();
3620 };
3621
3622
3623 /**
3624 * Resize the center position based on the current values in this.defaultXCenter
3625 * and this.defaultYCenter (which are strings with a percentage or a value
3626 * in pixels). The center positions are the variables this.xCenter
3627 * and this.yCenter
3628 */
3629 Graph3d.prototype._resizeCenter = function() {
3630 // calculate the horizontal center position
3631 if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
3632 this.xcenter =
3633 parseFloat(this.defaultXCenter) / 100 *
3634 this.frame.canvas.clientWidth;
3635 }
3636 else {
3637 this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
3638 }
3639
3640 // calculate the vertical center position
3641 if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
3642 this.ycenter =
3643 parseFloat(this.defaultYCenter) / 100 *
3644 (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
3645 }
3646 else {
3647 this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
3648 }
3649 };
3650
3651 /**
3652 * Set the rotation and distance of the camera
3653 * @param {Object} pos An object with the camera position. The object
3654 * contains three parameters:
3655 * - horizontal {Number}
3656 * The horizontal rotation, between 0 and 2*PI.
3657 * Optional, can be left undefined.
3658 * - vertical {Number}
3659 * The vertical rotation, between 0 and 0.5*PI
3660 * if vertical=0.5*PI, the graph is shown from the
3661 * top. Optional, can be left undefined.
3662 * - distance {Number}
3663 * The (normalized) distance of the camera to the
3664 * center of the graph, a value between 0.71 and 5.0.
3665 * Optional, can be left undefined.
3666 */
3667 Graph3d.prototype.setCameraPosition = function(pos) {
3668 if (pos === undefined) {
3669 return;
3670 }
3671
3672 if (pos.horizontal !== undefined && pos.vertical !== undefined) {
3673 this.camera.setArmRotation(pos.horizontal, pos.vertical);
3674 }
3675
3676 if (pos.distance !== undefined) {
3677 this.camera.setArmLength(pos.distance);
3678 }
3679
3680 this.redraw();
3681 };
3682
3683
3684 /**
3685 * Retrieve the current camera rotation
3686 * @return {object} An object with parameters horizontal, vertical, and
3687 * distance
3688 */
3689 Graph3d.prototype.getCameraPosition = function() {
3690 var pos = this.camera.getArmRotation();
3691 pos.distance = this.camera.getArmLength();
3692 return pos;
3693 };
3694
3695 /**
3696 * Load data into the 3D Graph
3697 */
3698 Graph3d.prototype._readData = function(data) {
3699 // read the data
3700 this._dataInitialize(data, this.style);
3701
3702
3703 if (this.dataFilter) {
3704 // apply filtering
3705 this.dataPoints = this.dataFilter._getDataPoints();
3706 }
3707 else {
3708 // no filtering. load all data
3709 this.dataPoints = this._getDataPoints(this.dataTable);
3710 }
3711
3712 // draw the filter
3713 this._redrawFilter();
3714 };
3715
3716 /**
3717 * Replace the dataset of the Graph3d
3718 * @param {Array | DataSet | DataView} data
3719 */
3720 Graph3d.prototype.setData = function (data) {
3721 this._readData(data);
3722 this.redraw();
3723
3724 // start animation when option is true
3725 if (this.animationAutoStart && this.dataFilter) {
3726 this.animationStart();
3727 }
3728 };
3729
3730 /**
3731 * Update the options. Options will be merged with current options
3732 * @param {Object} options
3733 */
3734 Graph3d.prototype.setOptions = function (options) {
3735 var cameraPosition = undefined;
3736
3737 this.animationStop();
3738
3739 if (options !== undefined) {
3740 // retrieve parameter values
3741 if (options.width !== undefined) this.width = options.width;
3742 if (options.height !== undefined) this.height = options.height;
3743
3744 if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
3745 if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
3746
3747 if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
3748 if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
3749 if (options.xLabel !== undefined) this.xLabel = options.xLabel;
3750 if (options.yLabel !== undefined) this.yLabel = options.yLabel;
3751 if (options.zLabel !== undefined) this.zLabel = options.zLabel;
3752
3753 if (options.style !== undefined) {
3754 var styleNumber = this._getStyleNumber(options.style);
3755 if (styleNumber !== -1) {
3756 this.style = styleNumber;
3757 }
3758 }
3759 if (options.showGrid !== undefined) this.showGrid = options.showGrid;
3760 if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
3761 if (options.showShadow !== undefined) this.showShadow = options.showShadow;
3762 if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
3763 if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
3764 if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
3765 if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
3766
3767 if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
3768 if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
3769 if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
3770
3771 if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
3772 if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
3773
3774 if (options.xMin !== undefined) this.defaultXMin = options.xMin;
3775 if (options.xStep !== undefined) this.defaultXStep = options.xStep;
3776 if (options.xMax !== undefined) this.defaultXMax = options.xMax;
3777 if (options.yMin !== undefined) this.defaultYMin = options.yMin;
3778 if (options.yStep !== undefined) this.defaultYStep = options.yStep;
3779 if (options.yMax !== undefined) this.defaultYMax = options.yMax;
3780 if (options.zMin !== undefined) this.defaultZMin = options.zMin;
3781 if (options.zStep !== undefined) this.defaultZStep = options.zStep;
3782 if (options.zMax !== undefined) this.defaultZMax = options.zMax;
3783 if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
3784 if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
3785
3786 if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
3787
3788 if (cameraPosition !== undefined) {
3789 this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
3790 this.camera.setArmLength(cameraPosition.distance);
3791 }
3792 else {
3793 this.camera.setArmRotation(1.0, 0.5);
3794 this.camera.setArmLength(1.7);
3795 }
3796 }
3797
3798 this._setBackgroundColor(options && options.backgroundColor);
3799
3800 this.setSize(this.width, this.height);
3801
3802 // re-load the data
3803 if (this.dataTable) {
3804 this.setData(this.dataTable);
3805 }
3806
3807 // start animation when option is true
3808 if (this.animationAutoStart && this.dataFilter) {
3809 this.animationStart();
3810 }
3811 };
3812
3813 /**
3814 * Redraw the Graph.
3815 */
3816 Graph3d.prototype.redraw = function() {
3817 if (this.dataPoints === undefined) {
3818 throw 'Error: graph data not initialized';
3819 }
3820
3821 this._resizeCanvas();
3822 this._resizeCenter();
3823 this._redrawSlider();
3824 this._redrawClear();
3825 this._redrawAxis();
3826
3827 if (this.style === Graph3d.STYLE.GRID ||
3828 this.style === Graph3d.STYLE.SURFACE) {
3829 this._redrawDataGrid();
3830 }
3831 else if (this.style === Graph3d.STYLE.LINE) {
3832 this._redrawDataLine();
3833 }
3834 else if (this.style === Graph3d.STYLE.BAR ||
3835 this.style === Graph3d.STYLE.BARCOLOR ||
3836 this.style === Graph3d.STYLE.BARSIZE) {
3837 this._redrawDataBar();
3838 }
3839 else {
3840 // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
3841 this._redrawDataDot();
3842 }
3843
3844 this._redrawInfo();
3845 this._redrawLegend();
3846 };
3847
3848 /**
3849 * Clear the canvas before redrawing
3850 */
3851 Graph3d.prototype._redrawClear = function() {
3852 var canvas = this.frame.canvas;
3853 var ctx = canvas.getContext('2d');
3854
3855 ctx.clearRect(0, 0, canvas.width, canvas.height);
3856 };
3857
3858
3859 /**
3860 * Redraw the legend showing the colors
3861 */
3862 Graph3d.prototype._redrawLegend = function() {
3863 var y;
3864
3865 if (this.style === Graph3d.STYLE.DOTCOLOR ||
3866 this.style === Graph3d.STYLE.DOTSIZE) {
3867
3868 var dotSize = this.frame.clientWidth * 0.02;
3869
3870 var widthMin, widthMax;
3871 if (this.style === Graph3d.STYLE.DOTSIZE) {
3872 widthMin = dotSize / 2; // px
3873 widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
3874 }
3875 else {
3876 widthMin = 20; // px
3877 widthMax = 20; // px
3878 }
3879
3880 var height = Math.max(this.frame.clientHeight * 0.25, 100);
3881 var top = this.margin;
3882 var right = this.frame.clientWidth - this.margin;
3883 var left = right - widthMax;
3884 var bottom = top + height;
3885 }
3886
3887 var canvas = this.frame.canvas;
3888 var ctx = canvas.getContext('2d');
3889 ctx.lineWidth = 1;
3890 ctx.font = '14px arial'; // TODO: put in options
3891
3892 if (this.style === Graph3d.STYLE.DOTCOLOR) {
3893 // draw the color bar
3894 var ymin = 0;
3895 var ymax = height; // Todo: make height customizable
3896 for (y = ymin; y < ymax; y++) {
3897 var f = (y - ymin) / (ymax - ymin);
3898
3899 //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
3900 var hue = f * 240;
3901 var color = this._hsv2rgb(hue, 1, 1);
3902
3903 ctx.strokeStyle = color;
3904 ctx.beginPath();
3905 ctx.moveTo(left, top + y);
3906 ctx.lineTo(right, top + y);
3907 ctx.stroke();
3908 }
3909
3910 ctx.strokeStyle = this.colorAxis;
3911 ctx.strokeRect(left, top, widthMax, height);
3912 }
3913
3914 if (this.style === Graph3d.STYLE.DOTSIZE) {
3915 // draw border around color bar
3916 ctx.strokeStyle = this.colorAxis;
3917 ctx.fillStyle = this.colorDot;
3918 ctx.beginPath();
3919 ctx.moveTo(left, top);
3920 ctx.lineTo(right, top);
3921 ctx.lineTo(right - widthMax + widthMin, bottom);
3922 ctx.lineTo(left, bottom);
3923 ctx.closePath();
3924 ctx.fill();
3925 ctx.stroke();
3926 }
3927
3928 if (this.style === Graph3d.STYLE.DOTCOLOR ||
3929 this.style === Graph3d.STYLE.DOTSIZE) {
3930 // print values along the color bar
3931 var gridLineLen = 5; // px
3932 var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
3933 step.start();
3934 if (step.getCurrent() < this.valueMin) {
3935 step.next();
3936 }
3937 while (!step.end()) {
3938 y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
3939
3940 ctx.beginPath();
3941 ctx.moveTo(left - gridLineLen, y);
3942 ctx.lineTo(left, y);
3943 ctx.stroke();
3944
3945 ctx.textAlign = 'right';
3946 ctx.textBaseline = 'middle';
3947 ctx.fillStyle = this.colorAxis;
3948 ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
3949
3950 step.next();
3951 }
3952
3953 ctx.textAlign = 'right';
3954 ctx.textBaseline = 'top';
3955 var label = this.legendLabel;
3956 ctx.fillText(label, right, bottom + this.margin);
3957 }
3958 };
3959
3960 /**
3961 * Redraw the filter
3962 */
3963 Graph3d.prototype._redrawFilter = function() {
3964 this.frame.filter.innerHTML = '';
3965
3966 if (this.dataFilter) {
3967 var options = {
3968 'visible': this.showAnimationControls
3969 };
3970 var slider = new Slider(this.frame.filter, options);
3971 this.frame.filter.slider = slider;
3972
3973 // TODO: css here is not nice here...
3974 this.frame.filter.style.padding = '10px';
3975 //this.frame.filter.style.backgroundColor = '#EFEFEF';
3976
3977 slider.setValues(this.dataFilter.values);
3978 slider.setPlayInterval(this.animationInterval);
3979
3980 // create an event handler
3981 var me = this;
3982 var onchange = function () {
3983 var index = slider.getIndex();
3984
3985 me.dataFilter.selectValue(index);
3986 me.dataPoints = me.dataFilter._getDataPoints();
3987
3988 me.redraw();
3989 };
3990 slider.setOnChangeCallback(onchange);
3991 }
3992 else {
3993 this.frame.filter.slider = undefined;
3994 }
3995 };
3996
3997 /**
3998 * Redraw the slider
3999 */
4000 Graph3d.prototype._redrawSlider = function() {
4001 if ( this.frame.filter.slider !== undefined) {
4002 this.frame.filter.slider.redraw();
4003 }
4004 };
4005
4006
4007 /**
4008 * Redraw common information
4009 */
4010 Graph3d.prototype._redrawInfo = function() {
4011 if (this.dataFilter) {
4012 var canvas = this.frame.canvas;
4013 var ctx = canvas.getContext('2d');
4014
4015 ctx.font = '14px arial'; // TODO: put in options
4016 ctx.lineStyle = 'gray';
4017 ctx.fillStyle = 'gray';
4018 ctx.textAlign = 'left';
4019 ctx.textBaseline = 'top';
4020
4021 var x = this.margin;
4022 var y = this.margin;
4023 ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
4024 }
4025 };
4026
4027
4028 /**
4029 * Redraw the axis
4030 */
4031 Graph3d.prototype._redrawAxis = function() {
4032 var canvas = this.frame.canvas,
4033 ctx = canvas.getContext('2d'),
4034 from, to, step, prettyStep,
4035 text, xText, yText, zText,
4036 offset, xOffset, yOffset,
4037 xMin2d, xMax2d;
4038
4039 // TODO: get the actual rendered style of the containerElement
4040 //ctx.font = this.containerElement.style.font;
4041 ctx.font = 24 / this.camera.getArmLength() + 'px arial';
4042
4043 // calculate the length for the short grid lines
4044 var gridLenX = 0.025 / this.scale.x;
4045 var gridLenY = 0.025 / this.scale.y;
4046 var textMargin = 5 / this.camera.getArmLength(); // px
4047 var armAngle = this.camera.getArmRotation().horizontal;
4048
4049 // draw x-grid lines
4050 ctx.lineWidth = 1;
4051 prettyStep = (this.defaultXStep === undefined);
4052 step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
4053 step.start();
4054 if (step.getCurrent() < this.xMin) {
4055 step.next();
4056 }
4057 while (!step.end()) {
4058 var x = step.getCurrent();
4059
4060 if (this.showGrid) {
4061 from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
4062 to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
4063 ctx.strokeStyle = this.colorGrid;
4064 ctx.beginPath();
4065 ctx.moveTo(from.x, from.y);
4066 ctx.lineTo(to.x, to.y);
4067 ctx.stroke();
4068 }
4069 else {
4070 from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
4071 to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
4072 ctx.strokeStyle = this.colorAxis;
4073 ctx.beginPath();
4074 ctx.moveTo(from.x, from.y);
4075 ctx.lineTo(to.x, to.y);
4076 ctx.stroke();
4077
4078 from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
4079 to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
4080 ctx.strokeStyle = this.colorAxis;
4081 ctx.beginPath();
4082 ctx.moveTo(from.x, from.y);
4083 ctx.lineTo(to.x, to.y);
4084 ctx.stroke();
4085 }
4086
4087 yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
4088 text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
4089 if (Math.cos(armAngle * 2) > 0) {
4090 ctx.textAlign = 'center';
4091 ctx.textBaseline = 'top';
4092 text.y += textMargin;
4093 }
4094 else if (Math.sin(armAngle * 2) < 0){
4095 ctx.textAlign = 'right';
4096 ctx.textBaseline = 'middle';
4097 }
4098 else {
4099 ctx.textAlign = 'left';
4100 ctx.textBaseline = 'middle';
4101 }
4102 ctx.fillStyle = this.colorAxis;
4103 ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
4104
4105 step.next();
4106 }
4107
4108 // draw y-grid lines
4109 ctx.lineWidth = 1;
4110 prettyStep = (this.defaultYStep === undefined);
4111 step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
4112 step.start();
4113 if (step.getCurrent() < this.yMin) {
4114 step.next();
4115 }
4116 while (!step.end()) {
4117 if (this.showGrid) {
4118 from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
4119 to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
4120 ctx.strokeStyle = this.colorGrid;
4121 ctx.beginPath();
4122 ctx.moveTo(from.x, from.y);
4123 ctx.lineTo(to.x, to.y);
4124 ctx.stroke();
4125 }
4126 else {
4127 from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
4128 to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
4129 ctx.strokeStyle = this.colorAxis;
4130 ctx.beginPath();
4131 ctx.moveTo(from.x, from.y);
4132 ctx.lineTo(to.x, to.y);
4133 ctx.stroke();
4134
4135 from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
4136 to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
4137 ctx.strokeStyle = this.colorAxis;
4138 ctx.beginPath();
4139 ctx.moveTo(from.x, from.y);
4140 ctx.lineTo(to.x, to.y);
4141 ctx.stroke();
4142 }
4143
4144 xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
4145 text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
4146 if (Math.cos(armAngle * 2) < 0) {
4147 ctx.textAlign = 'center';
4148 ctx.textBaseline = 'top';
4149 text.y += textMargin;
4150 }
4151 else if (Math.sin(armAngle * 2) > 0){
4152 ctx.textAlign = 'right';
4153 ctx.textBaseline = 'middle';
4154 }
4155 else {
4156 ctx.textAlign = 'left';
4157 ctx.textBaseline = 'middle';
4158 }
4159 ctx.fillStyle = this.colorAxis;
4160 ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
4161
4162 step.next();
4163 }
4164
4165 // draw z-grid lines and axis
4166 ctx.lineWidth = 1;
4167 prettyStep = (this.defaultZStep === undefined);
4168 step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
4169 step.start();
4170 if (step.getCurrent() < this.zMin) {
4171 step.next();
4172 }
4173 xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
4174 yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
4175 while (!step.end()) {
4176 // TODO: make z-grid lines really 3d?
4177 from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
4178 ctx.strokeStyle = this.colorAxis;
4179 ctx.beginPath();
4180 ctx.moveTo(from.x, from.y);
4181 ctx.lineTo(from.x - textMargin, from.y);
4182 ctx.stroke();
4183
4184 ctx.textAlign = 'right';
4185 ctx.textBaseline = 'middle';
4186 ctx.fillStyle = this.colorAxis;
4187 ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
4188
4189 step.next();
4190 }
4191 ctx.lineWidth = 1;
4192 from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
4193 to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
4194 ctx.strokeStyle = this.colorAxis;
4195 ctx.beginPath();
4196 ctx.moveTo(from.x, from.y);
4197 ctx.lineTo(to.x, to.y);
4198 ctx.stroke();
4199
4200 // draw x-axis
4201 ctx.lineWidth = 1;
4202 // line at yMin
4203 xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
4204 xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
4205 ctx.strokeStyle = this.colorAxis;
4206 ctx.beginPath();
4207 ctx.moveTo(xMin2d.x, xMin2d.y);
4208 ctx.lineTo(xMax2d.x, xMax2d.y);
4209 ctx.stroke();
4210 // line at ymax
4211 xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
4212 xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
4213 ctx.strokeStyle = this.colorAxis;
4214 ctx.beginPath();
4215 ctx.moveTo(xMin2d.x, xMin2d.y);
4216 ctx.lineTo(xMax2d.x, xMax2d.y);
4217 ctx.stroke();
4218
4219 // draw y-axis
4220 ctx.lineWidth = 1;
4221 // line at xMin
4222 from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
4223 to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
4224 ctx.strokeStyle = this.colorAxis;
4225 ctx.beginPath();
4226 ctx.moveTo(from.x, from.y);
4227 ctx.lineTo(to.x, to.y);
4228 ctx.stroke();
4229 // line at xMax
4230 from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
4231 to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
4232 ctx.strokeStyle = this.colorAxis;
4233 ctx.beginPath();
4234 ctx.moveTo(from.x, from.y);
4235 ctx.lineTo(to.x, to.y);
4236 ctx.stroke();
4237
4238 // draw x-label
4239 var xLabel = this.xLabel;
4240 if (xLabel.length > 0) {
4241 yOffset = 0.1 / this.scale.y;
4242 xText = (this.xMin + this.xMax) / 2;
4243 yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
4244 text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
4245 if (Math.cos(armAngle * 2) > 0) {
4246 ctx.textAlign = 'center';
4247 ctx.textBaseline = 'top';
4248 }
4249 else if (Math.sin(armAngle * 2) < 0){
4250 ctx.textAlign = 'right';
4251 ctx.textBaseline = 'middle';
4252 }
4253 else {
4254 ctx.textAlign = 'left';
4255 ctx.textBaseline = 'middle';
4256 }
4257 ctx.fillStyle = this.colorAxis;
4258 ctx.fillText(xLabel, text.x, text.y);
4259 }
4260
4261 // draw y-label
4262 var yLabel = this.yLabel;
4263 if (yLabel.length > 0) {
4264 xOffset = 0.1 / this.scale.x;
4265 xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
4266 yText = (this.yMin + this.yMax) / 2;
4267 text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
4268 if (Math.cos(armAngle * 2) < 0) {
4269 ctx.textAlign = 'center';
4270 ctx.textBaseline = 'top';
4271 }
4272 else if (Math.sin(armAngle * 2) > 0){
4273 ctx.textAlign = 'right';
4274 ctx.textBaseline = 'middle';
4275 }
4276 else {
4277 ctx.textAlign = 'left';
4278 ctx.textBaseline = 'middle';
4279 }
4280 ctx.fillStyle = this.colorAxis;
4281 ctx.fillText(yLabel, text.x, text.y);
4282 }
4283
4284 // draw z-label
4285 var zLabel = this.zLabel;
4286 if (zLabel.length > 0) {
4287 offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
4288 xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
4289 yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
4290 zText = (this.zMin + this.zMax) / 2;
4291 text = this._convert3Dto2D(new Point3d(xText, yText, zText));
4292 ctx.textAlign = 'right';
4293 ctx.textBaseline = 'middle';
4294 ctx.fillStyle = this.colorAxis;
4295 ctx.fillText(zLabel, text.x - offset, text.y);
4296 }
4297 };
4298
4299 /**
4300 * Calculate the color based on the given value.
4301 * @param {Number} H Hue, a value be between 0 and 360
4302 * @param {Number} S Saturation, a value between 0 and 1
4303 * @param {Number} V Value, a value between 0 and 1
4304 */
4305 Graph3d.prototype._hsv2rgb = function(H, S, V) {
4306 var R, G, B, C, Hi, X;
4307
4308 C = V * S;
4309 Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
4310 X = C * (1 - Math.abs(((H/60) % 2) - 1));
4311
4312 switch (Hi) {
4313 case 0: R = C; G = X; B = 0; break;
4314 case 1: R = X; G = C; B = 0; break;
4315 case 2: R = 0; G = C; B = X; break;
4316 case 3: R = 0; G = X; B = C; break;
4317 case 4: R = X; G = 0; B = C; break;
4318 case 5: R = C; G = 0; B = X; break;
4319
4320 default: R = 0; G = 0; B = 0; break;
4321 }
4322
4323 return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
4324 };
4325
4326
4327 /**
4328 * Draw all datapoints as a grid
4329 * This function can be used when the style is 'grid'
4330 */
4331 Graph3d.prototype._redrawDataGrid = function() {
4332 var canvas = this.frame.canvas,
4333 ctx = canvas.getContext('2d'),
4334 point, right, top, cross,
4335 i,
4336 topSideVisible, fillStyle, strokeStyle, lineWidth,
4337 h, s, v, zAvg;
4338
4339
4340 if (this.dataPoints === undefined || this.dataPoints.length <= 0)
4341 return; // TODO: throw exception?
4342
4343 // calculate the translations and screen position of all points
4344 for (i = 0; i < this.dataPoints.length; i++) {
4345 var trans = this._convertPointToTranslation(this.dataPoints[i].point);
4346 var screen = this._convertTranslationToScreen(trans);
4347
4348 this.dataPoints[i].trans = trans;
4349 this.dataPoints[i].screen = screen;
4350
4351 // calculate the translation of the point at the bottom (needed for sorting)
4352 var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
4353 this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
4354 }
4355
4356 // sort the points on depth of their (x,y) position (not on z)
4357 var sortDepth = function (a, b) {
4358 return b.dist - a.dist;
4359 };
4360 this.dataPoints.sort(sortDepth);
4361
4362 if (this.style === Graph3d.STYLE.SURFACE) {
4363 for (i = 0; i < this.dataPoints.length; i++) {
4364 point = this.dataPoints[i];
4365 right = this.dataPoints[i].pointRight;
4366 top = this.dataPoints[i].pointTop;
4367 cross = this.dataPoints[i].pointCross;
4368
4369 if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
4370
4371 if (this.showGrayBottom || this.showShadow) {
4372 // calculate the cross product of the two vectors from center
4373 // to left and right, in order to know whether we are looking at the
4374 // bottom or at the top side. We can also use the cross product
4375 // for calculating light intensity
4376 var aDiff = Point3d.subtract(cross.trans, point.trans);
4377 var bDiff = Point3d.subtract(top.trans, right.trans);
4378 var crossproduct = Point3d.crossProduct(aDiff, bDiff);
4379 var len = crossproduct.length();
4380 // FIXME: there is a bug with determining the surface side (shadow or colored)
4381
4382 topSideVisible = (crossproduct.z > 0);
4383 }
4384 else {
4385 topSideVisible = true;
4386 }
4387
4388 if (topSideVisible) {
4389 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
4390 zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
4391 h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
4392 s = 1; // saturation
4393
4394 if (this.showShadow) {
4395 v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
4396 fillStyle = this._hsv2rgb(h, s, v);
4397 strokeStyle = fillStyle;
4398 }
4399 else {
4400 v = 1;
4401 fillStyle = this._hsv2rgb(h, s, v);
4402 strokeStyle = this.colorAxis;
4403 }
4404 }
4405 else {
4406 fillStyle = 'gray';
4407 strokeStyle = this.colorAxis;
4408 }
4409 lineWidth = 0.5;
4410
4411 ctx.lineWidth = lineWidth;
4412 ctx.fillStyle = fillStyle;
4413 ctx.strokeStyle = strokeStyle;
4414 ctx.beginPath();
4415 ctx.moveTo(point.screen.x, point.screen.y);
4416 ctx.lineTo(right.screen.x, right.screen.y);
4417 ctx.lineTo(cross.screen.x, cross.screen.y);
4418 ctx.lineTo(top.screen.x, top.screen.y);
4419 ctx.closePath();
4420 ctx.fill();
4421 ctx.stroke();
4422 }
4423 }
4424 }
4425 else { // grid style
4426 for (i = 0; i < this.dataPoints.length; i++) {
4427 point = this.dataPoints[i];
4428 right = this.dataPoints[i].pointRight;
4429 top = this.dataPoints[i].pointTop;
4430
4431 if (point !== undefined) {
4432 if (this.showPerspective) {
4433 lineWidth = 2 / -point.trans.z;
4434 }
4435 else {
4436 lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
4437 }
4438 }
4439
4440 if (point !== undefined && right !== undefined) {
4441 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
4442 zAvg = (point.point.z + right.point.z) / 2;
4443 h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
4444
4445 ctx.lineWidth = lineWidth;
4446 ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
4447 ctx.beginPath();
4448 ctx.moveTo(point.screen.x, point.screen.y);
4449 ctx.lineTo(right.screen.x, right.screen.y);
4450 ctx.stroke();
4451 }
4452
4453 if (point !== undefined && top !== undefined) {
4454 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
4455 zAvg = (point.point.z + top.point.z) / 2;
4456 h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
4457
4458 ctx.lineWidth = lineWidth;
4459 ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
4460 ctx.beginPath();
4461 ctx.moveTo(point.screen.x, point.screen.y);
4462 ctx.lineTo(top.screen.x, top.screen.y);
4463 ctx.stroke();
4464 }
4465 }
4466 }
4467 };
4468
4469
4470 /**
4471 * Draw all datapoints as dots.
4472 * This function can be used when the style is 'dot' or 'dot-line'
4473 */
4474 Graph3d.prototype._redrawDataDot = function() {
4475 var canvas = this.frame.canvas;
4476 var ctx = canvas.getContext('2d');
4477 var i;
4478
4479 if (this.dataPoints === undefined || this.dataPoints.length <= 0)
4480 return; // TODO: throw exception?
4481
4482 // calculate the translations of all points
4483 for (i = 0; i < this.dataPoints.length; i++) {
4484 var trans = this._convertPointToTranslation(this.dataPoints[i].point);
4485 var screen = this._convertTranslationToScreen(trans);
4486 this.dataPoints[i].trans = trans;
4487 this.dataPoints[i].screen = screen;
4488
4489 // calculate the distance from the point at the bottom to the camera
4490 var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
4491 this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
4492 }
4493
4494 // order the translated points by depth
4495 var sortDepth = function (a, b) {
4496 return b.dist - a.dist;
4497 };
4498 this.dataPoints.sort(sortDepth);
4499
4500 // draw the datapoints as colored circles
4501 var dotSize = this.frame.clientWidth * 0.02; // px
4502 for (i = 0; i < this.dataPoints.length; i++) {
4503 var point = this.dataPoints[i];
4504
4505 if (this.style === Graph3d.STYLE.DOTLINE) {
4506 // draw a vertical line from the bottom to the graph value
4507 //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
4508 var from = this._convert3Dto2D(point.bottom);
4509 ctx.lineWidth = 1;
4510 ctx.strokeStyle = this.colorGrid;
4511 ctx.beginPath();
4512 ctx.moveTo(from.x, from.y);
4513 ctx.lineTo(point.screen.x, point.screen.y);
4514 ctx.stroke();
4515 }
4516
4517 // calculate radius for the circle
4518 var size;
4519 if (this.style === Graph3d.STYLE.DOTSIZE) {
4520 size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
4521 }
4522 else {
4523 size = dotSize;
4524 }
4525
4526 var radius;
4527 if (this.showPerspective) {
4528 radius = size / -point.trans.z;
4529 }
4530 else {
4531 radius = size * -(this.eye.z / this.camera.getArmLength());
4532 }
4533 if (radius < 0) {
4534 radius = 0;
4535 }
4536
4537 var hue, color, borderColor;
4538 if (this.style === Graph3d.STYLE.DOTCOLOR ) {
4539 // calculate the color based on the value
4540 hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
4541 color = this._hsv2rgb(hue, 1, 1);
4542 borderColor = this._hsv2rgb(hue, 1, 0.8);
4543 }
4544 else if (this.style === Graph3d.STYLE.DOTSIZE) {
4545 color = this.colorDot;
4546 borderColor = this.colorDotBorder;
4547 }
4548 else {
4549 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
4550 hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
4551 color = this._hsv2rgb(hue, 1, 1);
4552 borderColor = this._hsv2rgb(hue, 1, 0.8);
4553 }
4554
4555 // draw the circle
4556 ctx.lineWidth = 1.0;
4557 ctx.strokeStyle = borderColor;
4558 ctx.fillStyle = color;
4559 ctx.beginPath();
4560 ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
4561 ctx.fill();
4562 ctx.stroke();
4563 }
4564 };
4565
4566 /**
4567 * Draw all datapoints as bars.
4568 * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
4569 */
4570 Graph3d.prototype._redrawDataBar = function() {
4571 var canvas = this.frame.canvas;
4572 var ctx = canvas.getContext('2d');
4573 var i, j, surface, corners;
4574
4575 if (this.dataPoints === undefined || this.dataPoints.length <= 0)
4576 return; // TODO: throw exception?
4577
4578 // calculate the translations of all points
4579 for (i = 0; i < this.dataPoints.length; i++) {
4580 var trans = this._convertPointToTranslation(this.dataPoints[i].point);
4581 var screen = this._convertTranslationToScreen(trans);
4582 this.dataPoints[i].trans = trans;
4583 this.dataPoints[i].screen = screen;
4584
4585 // calculate the distance from the point at the bottom to the camera
4586 var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
4587 this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
4588 }
4589
4590 // order the translated points by depth
4591 var sortDepth = function (a, b) {
4592 return b.dist - a.dist;
4593 };
4594 this.dataPoints.sort(sortDepth);
4595
4596 // draw the datapoints as bars
4597 var xWidth = this.xBarWidth / 2;
4598 var yWidth = this.yBarWidth / 2;
4599 for (i = 0; i < this.dataPoints.length; i++) {
4600 var point = this.dataPoints[i];
4601
4602 // determine color
4603 var hue, color, borderColor;
4604 if (this.style === Graph3d.STYLE.BARCOLOR ) {
4605 // calculate the color based on the value
4606 hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
4607 color = this._hsv2rgb(hue, 1, 1);
4608 borderColor = this._hsv2rgb(hue, 1, 0.8);
4609 }
4610 else if (this.style === Graph3d.STYLE.BARSIZE) {
4611 color = this.colorDot;
4612 borderColor = this.colorDotBorder;
4613 }
4614 else {
4615 // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
4616 hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
4617 color = this._hsv2rgb(hue, 1, 1);
4618 borderColor = this._hsv2rgb(hue, 1, 0.8);
4619 }
4620
4621 // calculate size for the bar
4622 if (this.style === Graph3d.STYLE.BARSIZE) {
4623 xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
4624 yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
4625 }
4626
4627 // calculate all corner points
4628 var me = this;
4629 var point3d = point.point;
4630 var top = [
4631 {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
4632 {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
4633 {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
4634 {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
4635 ];
4636 var bottom = [
4637 {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
4638 {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
4639 {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
4640 {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
4641 ];
4642
4643 // calculate screen location of the points
4644 top.forEach(function (obj) {
4645 obj.screen = me._convert3Dto2D(obj.point);
4646 });
4647 bottom.forEach(function (obj) {
4648 obj.screen = me._convert3Dto2D(obj.point);
4649 });
4650
4651 // create five sides, calculate both corner points and center points
4652 var surfaces = [
4653 {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
4654 {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
4655 {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
4656 {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
4657 {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
4658 ];
4659 point.surfaces = surfaces;
4660
4661 // calculate the distance of each of the surface centers to the camera
4662 for (j = 0; j < surfaces.length; j++) {
4663 surface = surfaces[j];
4664 var transCenter = this._convertPointToTranslation(surface.center);
4665 surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
4666 // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
4667 // but the current solution is fast/simple and works in 99.9% of all cases
4668 // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
4669 }
4670
4671 // order the surfaces by their (translated) depth
4672 surfaces.sort(function (a, b) {
4673 var diff = b.dist - a.dist;
4674 if (diff) return diff;
4675
4676 // if equal depth, sort the top surface last
4677 if (a.corners === top) return 1;
4678 if (b.corners === top) return -1;
4679
4680 // both are equal
4681 return 0;
4682 });
4683
4684 // draw the ordered surfaces
4685 ctx.lineWidth = 1;
4686 ctx.strokeStyle = borderColor;
4687 ctx.fillStyle = color;
4688 // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
4689 for (j = 2; j < surfaces.length; j++) {
4690 surface = surfaces[j];
4691 corners = surface.corners;
4692 ctx.beginPath();
4693 ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
4694 ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
4695 ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
4696 ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
4697 ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
4698 ctx.fill();
4699 ctx.stroke();
4700 }
4701 }
4702 };
4703
4704
4705 /**
4706 * Draw a line through all datapoints.
4707 * This function can be used when the style is 'line'
4708 */
4709 Graph3d.prototype._redrawDataLine = function() {
4710 var canvas = this.frame.canvas,
4711 ctx = canvas.getContext('2d'),
4712 point, i;
4713
4714 if (this.dataPoints === undefined || this.dataPoints.length <= 0)
4715 return; // TODO: throw exception?
4716
4717 // calculate the translations of all points
4718 for (i = 0; i < this.dataPoints.length; i++) {
4719 var trans = this._convertPointToTranslation(this.dataPoints[i].point);
4720 var screen = this._convertTranslationToScreen(trans);
4721
4722 this.dataPoints[i].trans = trans;
4723 this.dataPoints[i].screen = screen;
4724 }
4725
4726 // start the line
4727 if (this.dataPoints.length > 0) {
4728 point = this.dataPoints[0];
4729
4730 ctx.lineWidth = 1; // TODO: make customizable
4731 ctx.strokeStyle = 'blue'; // TODO: make customizable
4732 ctx.beginPath();
4733 ctx.moveTo(point.screen.x, point.screen.y);
4734 }
4735
4736 // draw the datapoints as colored circles
4737 for (i = 1; i < this.dataPoints.length; i++) {
4738 point = this.dataPoints[i];
4739 ctx.lineTo(point.screen.x, point.screen.y);
4740 }
4741
4742 // finish the line
4743 if (this.dataPoints.length > 0) {
4744 ctx.stroke();
4745 }
4746 };
4747
4748 /**
4749 * Start a moving operation inside the provided parent element
4750 * @param {Event} event The event that occurred (required for
4751 * retrieving the mouse position)
4752 */
4753 Graph3d.prototype._onMouseDown = function(event) {
4754 event = event || window.event;
4755
4756 // check if mouse is still down (may be up when focus is lost for example
4757 // in an iframe)
4758 if (this.leftButtonDown) {
4759 this._onMouseUp(event);
4760 }
4761
4762 // only react on left mouse button down
4763 this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
4764 if (!this.leftButtonDown && !this.touchDown) return;
4765
4766 // get mouse position (different code for IE and all other browsers)
4767 this.startMouseX = getMouseX(event);
4768 this.startMouseY = getMouseY(event);
4769
4770 this.startStart = new Date(this.start);
4771 this.startEnd = new Date(this.end);
4772 this.startArmRotation = this.camera.getArmRotation();
4773
4774 this.frame.style.cursor = 'move';
4775
4776 // add event listeners to handle moving the contents
4777 // we store the function onmousemove and onmouseup in the graph, so we can
4778 // remove the eventlisteners lateron in the function mouseUp()
4779 var me = this;
4780 this.onmousemove = function (event) {me._onMouseMove(event);};
4781 this.onmouseup = function (event) {me._onMouseUp(event);};
4782 util.addEventListener(document, 'mousemove', me.onmousemove);
4783 util.addEventListener(document, 'mouseup', me.onmouseup);
4784 util.preventDefault(event);
4785 };
4786
4787
4788 /**
4789 * Perform moving operating.
4790 * This function activated from within the funcion Graph.mouseDown().
4791 * @param {Event} event Well, eehh, the event
4792 */
4793 Graph3d.prototype._onMouseMove = function (event) {
4794 event = event || window.event;
4795
4796 // calculate change in mouse position
4797 var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
4798 var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
4799
4800 var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
4801 var verticalNew = this.startArmRotation.vertical + diffY / 200;
4802
4803 var snapAngle = 4; // degrees
4804 var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
4805
4806 // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
4807 // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
4808 if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
4809 horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
4810 }
4811 if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
4812 horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
4813 }
4814
4815 // snap vertically to nice angles
4816 if (Math.abs(Math.sin(verticalNew)) < snapValue) {
4817 verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
4818 }
4819 if (Math.abs(Math.cos(verticalNew)) < snapValue) {
4820 verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
4821 }
4822
4823 this.camera.setArmRotation(horizontalNew, verticalNew);
4824 this.redraw();
4825
4826 // fire a cameraPositionChange event
4827 var parameters = this.getCameraPosition();
4828 this.emit('cameraPositionChange', parameters);
4829
4830 util.preventDefault(event);
4831 };
4832
4833
4834 /**
4835 * Stop moving operating.
4836 * This function activated from within the funcion Graph.mouseDown().
4837 * @param {event} event The event
4838 */
4839 Graph3d.prototype._onMouseUp = function (event) {
4840 this.frame.style.cursor = 'auto';
4841 this.leftButtonDown = false;
4842
4843 // remove event listeners here
4844 util.removeEventListener(document, 'mousemove', this.onmousemove);
4845 util.removeEventListener(document, 'mouseup', this.onmouseup);
4846 util.preventDefault(event);
4847 };
4848
4849 /**
4850 * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
4851 * @param {Event} event A mouse move event
4852 */
4853 Graph3d.prototype._onTooltip = function (event) {
4854 var delay = 300; // ms
4855 var mouseX = getMouseX(event) - util.getAbsoluteLeft(this.frame);
4856 var mouseY = getMouseY(event) - util.getAbsoluteTop(this.frame);
4857
4858 if (!this.showTooltip) {
4859 return;
4860 }
4861
4862 if (this.tooltipTimeout) {
4863 clearTimeout(this.tooltipTimeout);
4864 }
4865
4866 // (delayed) display of a tooltip only if no mouse button is down
4867 if (this.leftButtonDown) {
4868 this._hideTooltip();
4869 return;
4870 }
4871
4872 if (this.tooltip && this.tooltip.dataPoint) {
4873 // tooltip is currently visible
4874 var dataPoint = this._dataPointFromXY(mouseX, mouseY);
4875 if (dataPoint !== this.tooltip.dataPoint) {
4876 // datapoint changed
4877 if (dataPoint) {
4878 this._showTooltip(dataPoint);
4879 }
4880 else {
4881 this._hideTooltip();
4882 }
4883 }
4884 }
4885 else {
4886 // tooltip is currently not visible
4887 var me = this;
4888 this.tooltipTimeout = setTimeout(function () {
4889 me.tooltipTimeout = null;
4890
4891 // show a tooltip if we have a data point
4892 var dataPoint = me._dataPointFromXY(mouseX, mouseY);
4893 if (dataPoint) {
4894 me._showTooltip(dataPoint);
4895 }
4896 }, delay);
4897 }
4898 };
4899
4900 /**
4901 * Event handler for touchstart event on mobile devices
4902 */
4903 Graph3d.prototype._onTouchStart = function(event) {
4904 this.touchDown = true;
4905
4906 var me = this;
4907 this.ontouchmove = function (event) {me._onTouchMove(event);};
4908 this.ontouchend = function (event) {me._onTouchEnd(event);};
4909 util.addEventListener(document, 'touchmove', me.ontouchmove);
4910 util.addEventListener(document, 'touchend', me.ontouchend);
4911
4912 this._onMouseDown(event);
4913 };
4914
4915 /**
4916 * Event handler for touchmove event on mobile devices
4917 */
4918 Graph3d.prototype._onTouchMove = function(event) {
4919 this._onMouseMove(event);
4920 };
4921
4922 /**
4923 * Event handler for touchend event on mobile devices
4924 */
4925 Graph3d.prototype._onTouchEnd = function(event) {
4926 this.touchDown = false;
4927
4928 util.removeEventListener(document, 'touchmove', this.ontouchmove);
4929 util.removeEventListener(document, 'touchend', this.ontouchend);
4930
4931 this._onMouseUp(event);
4932 };
4933
4934
4935 /**
4936 * Event handler for mouse wheel event, used to zoom the graph
4937 * Code from http://adomas.org/javascript-mouse-wheel/
4938 * @param {event} event The event
4939 */
4940 Graph3d.prototype._onWheel = function(event) {
4941 if (!event) /* For IE. */
4942 event = window.event;
4943
4944 // retrieve delta
4945 var delta = 0;
4946 if (event.wheelDelta) { /* IE/Opera. */
4947 delta = event.wheelDelta/120;
4948 } else if (event.detail) { /* Mozilla case. */
4949 // In Mozilla, sign of delta is different than in IE.
4950 // Also, delta is multiple of 3.
4951 delta = -event.detail/3;
4952 }
4953
4954 // If delta is nonzero, handle it.
4955 // Basically, delta is now positive if wheel was scrolled up,
4956 // and negative, if wheel was scrolled down.
4957 if (delta) {
4958 var oldLength = this.camera.getArmLength();
4959 var newLength = oldLength * (1 - delta / 10);
4960
4961 this.camera.setArmLength(newLength);
4962 this.redraw();
4963
4964 this._hideTooltip();
4965 }
4966
4967 // fire a cameraPositionChange event
4968 var parameters = this.getCameraPosition();
4969 this.emit('cameraPositionChange', parameters);
4970
4971 // Prevent default actions caused by mouse wheel.
4972 // That might be ugly, but we handle scrolls somehow
4973 // anyway, so don't bother here..
4974 util.preventDefault(event);
4975 };
4976
4977 /**
4978 * Test whether a point lies inside given 2D triangle
4979 * @param {Point2d} point
4980 * @param {Point2d[]} triangle
4981 * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
4982 * @private
4983 */
4984 Graph3d.prototype._insideTriangle = function (point, triangle) {
4985 var a = triangle[0],
4986 b = triangle[1],
4987 c = triangle[2];
4988
4989 function sign (x) {
4990 return x > 0 ? 1 : x < 0 ? -1 : 0;
4991 }
4992
4993 var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
4994 var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
4995 var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
4996
4997 // each of the three signs must be either equal to each other or zero
4998 return (as == 0 || bs == 0 || as == bs) &&
4999 (bs == 0 || cs == 0 || bs == cs) &&
5000 (as == 0 || cs == 0 || as == cs);
5001 };
5002
5003 /**
5004 * Find a data point close to given screen position (x, y)
5005 * @param {Number} x
5006 * @param {Number} y
5007 * @return {Object | null} The closest data point or null if not close to any data point
5008 * @private
5009 */
5010 Graph3d.prototype._dataPointFromXY = function (x, y) {
5011 var i,
5012 distMax = 100, // px
5013 dataPoint = null,
5014 closestDataPoint = null,
5015 closestDist = null,
5016 center = new Point2d(x, y);
5017
5018 if (this.style === Graph3d.STYLE.BAR ||
5019 this.style === Graph3d.STYLE.BARCOLOR ||
5020 this.style === Graph3d.STYLE.BARSIZE) {
5021 // the data points are ordered from far away to closest
5022 for (i = this.dataPoints.length - 1; i >= 0; i--) {
5023 dataPoint = this.dataPoints[i];
5024 var surfaces = dataPoint.surfaces;
5025 if (surfaces) {
5026 for (var s = surfaces.length - 1; s >= 0; s--) {
5027 // split each surface in two triangles, and see if the center point is inside one of these
5028 var surface = surfaces[s];
5029 var corners = surface.corners;
5030 var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
5031 var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
5032 if (this._insideTriangle(center, triangle1) ||
5033 this._insideTriangle(center, triangle2)) {
5034 // return immediately at the first hit
5035 return dataPoint;
5036 }
5037 }
5038 }
5039 }
5040 }
5041 else {
5042 // find the closest data point, using distance to the center of the point on 2d screen
5043 for (i = 0; i < this.dataPoints.length; i++) {
5044 dataPoint = this.dataPoints[i];
5045 var point = dataPoint.screen;
5046 if (point) {
5047 var distX = Math.abs(x - point.x);
5048 var distY = Math.abs(y - point.y);
5049 var dist = Math.sqrt(distX * distX + distY * distY);
5050
5051 if ((closestDist === null || dist < closestDist) && dist < distMax) {
5052 closestDist = dist;
5053 closestDataPoint = dataPoint;
5054 }
5055 }
5056 }
5057 }
5058
5059
5060 return closestDataPoint;
5061 };
5062
5063 /**
5064 * Display a tooltip for given data point
5065 * @param {Object} dataPoint
5066 * @private
5067 */
5068 Graph3d.prototype._showTooltip = function (dataPoint) {
5069 var content, line, dot;
5070
5071 if (!this.tooltip) {
5072 content = document.createElement('div');
5073 content.style.position = 'absolute';
5074 content.style.padding = '10px';
5075 content.style.border = '1px solid #4d4d4d';
5076 content.style.color = '#1a1a1a';
5077 content.style.background = 'rgba(255,255,255,0.7)';
5078 content.style.borderRadius = '2px';
5079 content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
5080
5081 line = document.createElement('div');
5082 line.style.position = 'absolute';
5083 line.style.height = '40px';
5084 line.style.width = '0';
5085 line.style.borderLeft = '1px solid #4d4d4d';
5086
5087 dot = document.createElement('div');
5088 dot.style.position = 'absolute';
5089 dot.style.height = '0';
5090 dot.style.width = '0';
5091 dot.style.border = '5px solid #4d4d4d';
5092 dot.style.borderRadius = '5px';
5093
5094 this.tooltip = {
5095 dataPoint: null,
5096 dom: {
5097 content: content,
5098 line: line,
5099 dot: dot
5100 }
5101 };
5102 }
5103 else {
5104 content = this.tooltip.dom.content;
5105 line = this.tooltip.dom.line;
5106 dot = this.tooltip.dom.dot;
5107 }
5108
5109 this._hideTooltip();
5110
5111 this.tooltip.dataPoint = dataPoint;
5112 if (typeof this.showTooltip === 'function') {
5113 content.innerHTML = this.showTooltip(dataPoint.point);
5114 }
5115 else {
5116 content.innerHTML = '<table>' +
5117 '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
5118 '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
5119 '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
5120 '</table>';
5121 }
5122
5123 content.style.left = '0';
5124 content.style.top = '0';
5125 this.frame.appendChild(content);
5126 this.frame.appendChild(line);
5127 this.frame.appendChild(dot);
5128
5129 // calculate sizes
5130 var contentWidth = content.offsetWidth;
5131 var contentHeight = content.offsetHeight;
5132 var lineHeight = line.offsetHeight;
5133 var dotWidth = dot.offsetWidth;
5134 var dotHeight = dot.offsetHeight;
5135
5136 var left = dataPoint.screen.x - contentWidth / 2;
5137 left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
5138
5139 line.style.left = dataPoint.screen.x + 'px';
5140 line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
5141 content.style.left = left + 'px';
5142 content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
5143 dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
5144 dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
5145 };
5146
5147 /**
5148 * Hide the tooltip when displayed
5149 * @private
5150 */
5151 Graph3d.prototype._hideTooltip = function () {
5152 if (this.tooltip) {
5153 this.tooltip.dataPoint = null;
5154
5155 for (var prop in this.tooltip.dom) {
5156 if (this.tooltip.dom.hasOwnProperty(prop)) {
5157 var elem = this.tooltip.dom[prop];
5158 if (elem && elem.parentNode) {
5159 elem.parentNode.removeChild(elem);
5160 }
5161 }
5162 }
5163 }
5164 };
5165
5166 /**--------------------------------------------------------------------------**/
5167
5168
5169 /**
5170 * Get the horizontal mouse position from a mouse event
5171 * @param {Event} event
5172 * @return {Number} mouse x
5173 */
5174 getMouseX = function(event) {
5175 if ('clientX' in event) return event.clientX;
5176 return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
5177 };
5178
5179 /**
5180 * Get the vertical mouse position from a mouse event
5181 * @param {Event} event
5182 * @return {Number} mouse y
5183 */
5184 getMouseY = function(event) {
5185 if ('clientY' in event) return event.clientY;
5186 return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
5187 };
5188
5189 module.exports = Graph3d;
5190
5191
5192/***/ },
5193/* 6 */
5194/***/ function(module, exports, __webpack_require__) {
5195
5196 var Point3d = __webpack_require__(9);
5197
5198 /**
5199 * @class Camera
5200 * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
5201 * The camera is always looking in the direction of the origin of the arm.
5202 * This way, the camera always rotates around one fixed point, the location
5203 * of the camera arm.
5204 *
5205 * Documentation:
5206 * http://en.wikipedia.org/wiki/3D_projection
5207 */
5208 Camera = function () {
5209 this.armLocation = new Point3d();
5210 this.armRotation = {};
5211 this.armRotation.horizontal = 0;
5212 this.armRotation.vertical = 0;
5213 this.armLength = 1.7;
5214
5215 this.cameraLocation = new Point3d();
5216 this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
5217
5218 this.calculateCameraOrientation();
5219 };
5220
5221 /**
5222 * Set the location (origin) of the arm
5223 * @param {Number} x Normalized value of x
5224 * @param {Number} y Normalized value of y
5225 * @param {Number} z Normalized value of z
5226 */
5227 Camera.prototype.setArmLocation = function(x, y, z) {
5228 this.armLocation.x = x;
5229 this.armLocation.y = y;
5230 this.armLocation.z = z;
5231
5232 this.calculateCameraOrientation();
5233 };
5234
5235 /**
5236 * Set the rotation of the camera arm
5237 * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
5238 * Optional, can be left undefined.
5239 * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
5240 * if vertical=0.5*PI, the graph is shown from the
5241 * top. Optional, can be left undefined.
5242 */
5243 Camera.prototype.setArmRotation = function(horizontal, vertical) {
5244 if (horizontal !== undefined) {
5245 this.armRotation.horizontal = horizontal;
5246 }
5247
5248 if (vertical !== undefined) {
5249 this.armRotation.vertical = vertical;
5250 if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
5251 if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
5252 }
5253
5254 if (horizontal !== undefined || vertical !== undefined) {
5255 this.calculateCameraOrientation();
5256 }
5257 };
5258
5259 /**
5260 * Retrieve the current arm rotation
5261 * @return {object} An object with parameters horizontal and vertical
5262 */
5263 Camera.prototype.getArmRotation = function() {
5264 var rot = {};
5265 rot.horizontal = this.armRotation.horizontal;
5266 rot.vertical = this.armRotation.vertical;
5267
5268 return rot;
5269 };
5270
5271 /**
5272 * Set the (normalized) length of the camera arm.
5273 * @param {Number} length A length between 0.71 and 5.0
5274 */
5275 Camera.prototype.setArmLength = function(length) {
5276 if (length === undefined)
5277 return;
5278
5279 this.armLength = length;
5280
5281 // Radius must be larger than the corner of the graph,
5282 // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
5283 // graph
5284 if (this.armLength < 0.71) this.armLength = 0.71;
5285 if (this.armLength > 5.0) this.armLength = 5.0;
5286
5287 this.calculateCameraOrientation();
5288 };
5289
5290 /**
5291 * Retrieve the arm length
5292 * @return {Number} length
5293 */
5294 Camera.prototype.getArmLength = function() {
5295 return this.armLength;
5296 };
5297
5298 /**
5299 * Retrieve the camera location
5300 * @return {Point3d} cameraLocation
5301 */
5302 Camera.prototype.getCameraLocation = function() {
5303 return this.cameraLocation;
5304 };
5305
5306 /**
5307 * Retrieve the camera rotation
5308 * @return {Point3d} cameraRotation
5309 */
5310 Camera.prototype.getCameraRotation = function() {
5311 return this.cameraRotation;
5312 };
5313
5314 /**
5315 * Calculate the location and rotation of the camera based on the
5316 * position and orientation of the camera arm
5317 */
5318 Camera.prototype.calculateCameraOrientation = function() {
5319 // calculate location of the camera
5320 this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
5321 this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
5322 this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
5323
5324 // calculate rotation of the camera
5325 this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
5326 this.cameraRotation.y = 0;
5327 this.cameraRotation.z = -this.armRotation.horizontal;
5328 };
5329
5330 module.exports = Camera;
5331
5332/***/ },
5333/* 7 */
5334/***/ function(module, exports, __webpack_require__) {
5335
5336 var DataView = __webpack_require__(4);
5337
5338 /**
5339 * @class Filter
5340 *
5341 * @param {DataSet} data The google data table
5342 * @param {Number} column The index of the column to be filtered
5343 * @param {Graph} graph The graph
5344 */
5345 function Filter (data, column, graph) {
5346 this.data = data;
5347 this.column = column;
5348 this.graph = graph; // the parent graph
5349
5350 this.index = undefined;
5351 this.value = undefined;
5352
5353 // read all distinct values and select the first one
5354 this.values = graph.getDistinctValues(data.get(), this.column);
5355
5356 // sort both numeric and string values correctly
5357 this.values.sort(function (a, b) {
5358 return a > b ? 1 : a < b ? -1 : 0;
5359 });
5360
5361 if (this.values.length > 0) {
5362 this.selectValue(0);
5363 }
5364
5365 // create an array with the filtered datapoints. this will be loaded afterwards
5366 this.dataPoints = [];
5367
5368 this.loaded = false;
5369 this.onLoadCallback = undefined;
5370
5371 if (graph.animationPreload) {
5372 this.loaded = false;
5373 this.loadInBackground();
5374 }
5375 else {
5376 this.loaded = true;
5377 }
5378 };
5379
5380
5381 /**
5382 * Return the label
5383 * @return {string} label
5384 */
5385 Filter.prototype.isLoaded = function() {
5386 return this.loaded;
5387 };
5388
5389
5390 /**
5391 * Return the loaded progress
5392 * @return {Number} percentage between 0 and 100
5393 */
5394 Filter.prototype.getLoadedProgress = function() {
5395 var len = this.values.length;
5396
5397 var i = 0;
5398 while (this.dataPoints[i]) {
5399 i++;
5400 }
5401
5402 return Math.round(i / len * 100);
5403 };
5404
5405
5406 /**
5407 * Return the label
5408 * @return {string} label
5409 */
5410 Filter.prototype.getLabel = function() {
5411 return this.graph.filterLabel;
5412 };
5413
5414
5415 /**
5416 * Return the columnIndex of the filter
5417 * @return {Number} columnIndex
5418 */
5419 Filter.prototype.getColumn = function() {
5420 return this.column;
5421 };
5422
5423 /**
5424 * Return the currently selected value. Returns undefined if there is no selection
5425 * @return {*} value
5426 */
5427 Filter.prototype.getSelectedValue = function() {
5428 if (this.index === undefined)
5429 return undefined;
5430
5431 return this.values[this.index];
5432 };
5433
5434 /**
5435 * Retrieve all values of the filter
5436 * @return {Array} values
5437 */
5438 Filter.prototype.getValues = function() {
5439 return this.values;
5440 };
5441
5442 /**
5443 * Retrieve one value of the filter
5444 * @param {Number} index
5445 * @return {*} value
5446 */
5447 Filter.prototype.getValue = function(index) {
5448 if (index >= this.values.length)
5449 throw 'Error: index out of range';
5450
5451 return this.values[index];
5452 };
5453
5454
5455 /**
5456 * Retrieve the (filtered) dataPoints for the currently selected filter index
5457 * @param {Number} [index] (optional)
5458 * @return {Array} dataPoints
5459 */
5460 Filter.prototype._getDataPoints = function(index) {
5461 if (index === undefined)
5462 index = this.index;
5463
5464 if (index === undefined)
5465 return [];
5466
5467 var dataPoints;
5468 if (this.dataPoints[index]) {
5469 dataPoints = this.dataPoints[index];
5470 }
5471 else {
5472 var f = {};
5473 f.column = this.column;
5474 f.value = this.values[index];
5475
5476 var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
5477 dataPoints = this.graph._getDataPoints(dataView);
5478
5479 this.dataPoints[index] = dataPoints;
5480 }
5481
5482 return dataPoints;
5483 };
5484
5485
5486
5487 /**
5488 * Set a callback function when the filter is fully loaded.
5489 */
5490 Filter.prototype.setOnLoadCallback = function(callback) {
5491 this.onLoadCallback = callback;
5492 };
5493
5494
5495 /**
5496 * Add a value to the list with available values for this filter
5497 * No double entries will be created.
5498 * @param {Number} index
5499 */
5500 Filter.prototype.selectValue = function(index) {
5501 if (index >= this.values.length)
5502 throw 'Error: index out of range';
5503
5504 this.index = index;
5505 this.value = this.values[index];
5506 };
5507
5508 /**
5509 * Load all filtered rows in the background one by one
5510 * Start this method without providing an index!
5511 */
5512 Filter.prototype.loadInBackground = function(index) {
5513 if (index === undefined)
5514 index = 0;
5515
5516 var frame = this.graph.frame;
5517
5518 if (index < this.values.length) {
5519 var dataPointsTemp = this._getDataPoints(index);
5520 //this.graph.redrawInfo(); // TODO: not neat
5521
5522 // create a progress box
5523 if (frame.progress === undefined) {
5524 frame.progress = document.createElement('DIV');
5525 frame.progress.style.position = 'absolute';
5526 frame.progress.style.color = 'gray';
5527 frame.appendChild(frame.progress);
5528 }
5529 var progress = this.getLoadedProgress();
5530 frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
5531 // TODO: this is no nice solution...
5532 frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider
5533 frame.progress.style.left = 10 + 'px';
5534
5535 var me = this;
5536 setTimeout(function() {me.loadInBackground(index+1);}, 10);
5537 this.loaded = false;
5538 }
5539 else {
5540 this.loaded = true;
5541
5542 // remove the progress box
5543 if (frame.progress !== undefined) {
5544 frame.removeChild(frame.progress);
5545 frame.progress = undefined;
5546 }
5547
5548 if (this.onLoadCallback)
5549 this.onLoadCallback();
5550 }
5551 };
5552
5553 module.exports = Filter;
5554
5555
5556/***/ },
5557/* 8 */
5558/***/ function(module, exports, __webpack_require__) {
5559
5560 /**
5561 * @prototype Point2d
5562 * @param {Number} [x]
5563 * @param {Number} [y]
5564 */
5565 Point2d = function (x, y) {
5566 this.x = x !== undefined ? x : 0;
5567 this.y = y !== undefined ? y : 0;
5568 };
5569
5570 module.exports = Point2d;
5571
5572
5573/***/ },
5574/* 9 */
5575/***/ function(module, exports, __webpack_require__) {
5576
5577 /**
5578 * @prototype Point3d
5579 * @param {Number} [x]
5580 * @param {Number} [y]
5581 * @param {Number} [z]
5582 */
5583 function Point3d(x, y, z) {
5584 this.x = x !== undefined ? x : 0;
5585 this.y = y !== undefined ? y : 0;
5586 this.z = z !== undefined ? z : 0;
5587 };
5588
5589 /**
5590 * Subtract the two provided points, returns a-b
5591 * @param {Point3d} a
5592 * @param {Point3d} b
5593 * @return {Point3d} a-b
5594 */
5595 Point3d.subtract = function(a, b) {
5596 var sub = new Point3d();
5597 sub.x = a.x - b.x;
5598 sub.y = a.y - b.y;
5599 sub.z = a.z - b.z;
5600 return sub;
5601 };
5602
5603 /**
5604 * Add the two provided points, returns a+b
5605 * @param {Point3d} a
5606 * @param {Point3d} b
5607 * @return {Point3d} a+b
5608 */
5609 Point3d.add = function(a, b) {
5610 var sum = new Point3d();
5611 sum.x = a.x + b.x;
5612 sum.y = a.y + b.y;
5613 sum.z = a.z + b.z;
5614 return sum;
5615 };
5616
5617 /**
5618 * Calculate the average of two 3d points
5619 * @param {Point3d} a
5620 * @param {Point3d} b
5621 * @return {Point3d} The average, (a+b)/2
5622 */
5623 Point3d.avg = function(a, b) {
5624 return new Point3d(
5625 (a.x + b.x) / 2,
5626 (a.y + b.y) / 2,
5627 (a.z + b.z) / 2
5628 );
5629 };
5630
5631 /**
5632 * Calculate the cross product of the two provided points, returns axb
5633 * Documentation: http://en.wikipedia.org/wiki/Cross_product
5634 * @param {Point3d} a
5635 * @param {Point3d} b
5636 * @return {Point3d} cross product axb
5637 */
5638 Point3d.crossProduct = function(a, b) {
5639 var crossproduct = new Point3d();
5640
5641 crossproduct.x = a.y * b.z - a.z * b.y;
5642 crossproduct.y = a.z * b.x - a.x * b.z;
5643 crossproduct.z = a.x * b.y - a.y * b.x;
5644
5645 return crossproduct;
5646 };
5647
5648
5649 /**
5650 * Rtrieve the length of the vector (or the distance from this point to the origin
5651 * @return {Number} length
5652 */
5653 Point3d.prototype.length = function() {
5654 return Math.sqrt(
5655 this.x * this.x +
5656 this.y * this.y +
5657 this.z * this.z
5658 );
5659 };
5660
5661 module.exports = Point3d;
5662
5663
5664/***/ },
5665/* 10 */
5666/***/ function(module, exports, __webpack_require__) {
5667
5668 var util = __webpack_require__(1);
5669
5670 /**
5671 * @constructor Slider
5672 *
5673 * An html slider control with start/stop/prev/next buttons
5674 * @param {Element} container The element where the slider will be created
5675 * @param {Object} options Available options:
5676 * {boolean} visible If true (default) the
5677 * slider is visible.
5678 */
5679 function Slider(container, options) {
5680 if (container === undefined) {
5681 throw 'Error: No container element defined';
5682 }
5683 this.container = container;
5684 this.visible = (options && options.visible != undefined) ? options.visible : true;
5685
5686 if (this.visible) {
5687 this.frame = document.createElement('DIV');
5688 //this.frame.style.backgroundColor = '#E5E5E5';
5689 this.frame.style.width = '100%';
5690 this.frame.style.position = 'relative';
5691 this.container.appendChild(this.frame);
5692
5693 this.frame.prev = document.createElement('INPUT');
5694 this.frame.prev.type = 'BUTTON';
5695 this.frame.prev.value = 'Prev';
5696 this.frame.appendChild(this.frame.prev);
5697
5698 this.frame.play = document.createElement('INPUT');
5699 this.frame.play.type = 'BUTTON';
5700 this.frame.play.value = 'Play';
5701 this.frame.appendChild(this.frame.play);
5702
5703 this.frame.next = document.createElement('INPUT');
5704 this.frame.next.type = 'BUTTON';
5705 this.frame.next.value = 'Next';
5706 this.frame.appendChild(this.frame.next);
5707
5708 this.frame.bar = document.createElement('INPUT');
5709 this.frame.bar.type = 'BUTTON';
5710 this.frame.bar.style.position = 'absolute';
5711 this.frame.bar.style.border = '1px solid red';
5712 this.frame.bar.style.width = '100px';
5713 this.frame.bar.style.height = '6px';
5714 this.frame.bar.style.borderRadius = '2px';
5715 this.frame.bar.style.MozBorderRadius = '2px';
5716 this.frame.bar.style.border = '1px solid #7F7F7F';
5717 this.frame.bar.style.backgroundColor = '#E5E5E5';
5718 this.frame.appendChild(this.frame.bar);
5719
5720 this.frame.slide = document.createElement('INPUT');
5721 this.frame.slide.type = 'BUTTON';
5722 this.frame.slide.style.margin = '0px';
5723 this.frame.slide.value = ' ';
5724 this.frame.slide.style.position = 'relative';
5725 this.frame.slide.style.left = '-100px';
5726 this.frame.appendChild(this.frame.slide);
5727
5728 // create events
5729 var me = this;
5730 this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
5731 this.frame.prev.onclick = function (event) {me.prev(event);};
5732 this.frame.play.onclick = function (event) {me.togglePlay(event);};
5733 this.frame.next.onclick = function (event) {me.next(event);};
5734 }
5735
5736 this.onChangeCallback = undefined;
5737
5738 this.values = [];
5739 this.index = undefined;
5740
5741 this.playTimeout = undefined;
5742 this.playInterval = 1000; // milliseconds
5743 this.playLoop = true;
5744 }
5745
5746 /**
5747 * Select the previous index
5748 */
5749 Slider.prototype.prev = function() {
5750 var index = this.getIndex();
5751 if (index > 0) {
5752 index--;
5753 this.setIndex(index);
5754 }
5755 };
5756
5757 /**
5758 * Select the next index
5759 */
5760 Slider.prototype.next = function() {
5761 var index = this.getIndex();
5762 if (index < this.values.length - 1) {
5763 index++;
5764 this.setIndex(index);
5765 }
5766 };
5767
5768 /**
5769 * Select the next index
5770 */
5771 Slider.prototype.playNext = function() {
5772 var start = new Date();
5773
5774 var index = this.getIndex();
5775 if (index < this.values.length - 1) {
5776 index++;
5777 this.setIndex(index);
5778 }
5779 else if (this.playLoop) {
5780 // jump to the start
5781 index = 0;
5782 this.setIndex(index);
5783 }
5784
5785 var end = new Date();
5786 var diff = (end - start);
5787
5788 // calculate how much time it to to set the index and to execute the callback
5789 // function.
5790 var interval = Math.max(this.playInterval - diff, 0);
5791 // document.title = diff // TODO: cleanup
5792
5793 var me = this;
5794 this.playTimeout = setTimeout(function() {me.playNext();}, interval);
5795 };
5796
5797 /**
5798 * Toggle start or stop playing
5799 */
5800 Slider.prototype.togglePlay = function() {
5801 if (this.playTimeout === undefined) {
5802 this.play();
5803 } else {
5804 this.stop();
5805 }
5806 };
5807
5808 /**
5809 * Start playing
5810 */
5811 Slider.prototype.play = function() {
5812 // Test whether already playing
5813 if (this.playTimeout) return;
5814
5815 this.playNext();
5816
5817 if (this.frame) {
5818 this.frame.play.value = 'Stop';
5819 }
5820 };
5821
5822 /**
5823 * Stop playing
5824 */
5825 Slider.prototype.stop = function() {
5826 clearInterval(this.playTimeout);
5827 this.playTimeout = undefined;
5828
5829 if (this.frame) {
5830 this.frame.play.value = 'Play';
5831 }
5832 };
5833
5834 /**
5835 * Set a callback function which will be triggered when the value of the
5836 * slider bar has changed.
5837 */
5838 Slider.prototype.setOnChangeCallback = function(callback) {
5839 this.onChangeCallback = callback;
5840 };
5841
5842 /**
5843 * Set the interval for playing the list
5844 * @param {Number} interval The interval in milliseconds
5845 */
5846 Slider.prototype.setPlayInterval = function(interval) {
5847 this.playInterval = interval;
5848 };
5849
5850 /**
5851 * Retrieve the current play interval
5852 * @return {Number} interval The interval in milliseconds
5853 */
5854 Slider.prototype.getPlayInterval = function(interval) {
5855 return this.playInterval;
5856 };
5857
5858 /**
5859 * Set looping on or off
5860 * @pararm {boolean} doLoop If true, the slider will jump to the start when
5861 * the end is passed, and will jump to the end
5862 * when the start is passed.
5863 */
5864 Slider.prototype.setPlayLoop = function(doLoop) {
5865 this.playLoop = doLoop;
5866 };
5867
5868
5869 /**
5870 * Execute the onchange callback function
5871 */
5872 Slider.prototype.onChange = function() {
5873 if (this.onChangeCallback !== undefined) {
5874 this.onChangeCallback();
5875 }
5876 };
5877
5878 /**
5879 * redraw the slider on the correct place
5880 */
5881 Slider.prototype.redraw = function() {
5882 if (this.frame) {
5883 // resize the bar
5884 this.frame.bar.style.top = (this.frame.clientHeight/2 -
5885 this.frame.bar.offsetHeight/2) + 'px';
5886 this.frame.bar.style.width = (this.frame.clientWidth -
5887 this.frame.prev.clientWidth -
5888 this.frame.play.clientWidth -
5889 this.frame.next.clientWidth - 30) + 'px';
5890
5891 // position the slider button
5892 var left = this.indexToLeft(this.index);
5893 this.frame.slide.style.left = (left) + 'px';
5894 }
5895 };
5896
5897
5898 /**
5899 * Set the list with values for the slider
5900 * @param {Array} values A javascript array with values (any type)
5901 */
5902 Slider.prototype.setValues = function(values) {
5903 this.values = values;
5904
5905 if (this.values.length > 0)
5906 this.setIndex(0);
5907 else
5908 this.index = undefined;
5909 };
5910
5911 /**
5912 * Select a value by its index
5913 * @param {Number} index
5914 */
5915 Slider.prototype.setIndex = function(index) {
5916 if (index < this.values.length) {
5917 this.index = index;
5918
5919 this.redraw();
5920 this.onChange();
5921 }
5922 else {
5923 throw 'Error: index out of range';
5924 }
5925 };
5926
5927 /**
5928 * retrieve the index of the currently selected vaue
5929 * @return {Number} index
5930 */
5931 Slider.prototype.getIndex = function() {
5932 return this.index;
5933 };
5934
5935
5936 /**
5937 * retrieve the currently selected value
5938 * @return {*} value
5939 */
5940 Slider.prototype.get = function() {
5941 return this.values[this.index];
5942 };
5943
5944
5945 Slider.prototype._onMouseDown = function(event) {
5946 // only react on left mouse button down
5947 var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
5948 if (!leftButtonDown) return;
5949
5950 this.startClientX = event.clientX;
5951 this.startSlideX = parseFloat(this.frame.slide.style.left);
5952
5953 this.frame.style.cursor = 'move';
5954
5955 // add event listeners to handle moving the contents
5956 // we store the function onmousemove and onmouseup in the graph, so we can
5957 // remove the eventlisteners lateron in the function mouseUp()
5958 var me = this;
5959 this.onmousemove = function (event) {me._onMouseMove(event);};
5960 this.onmouseup = function (event) {me._onMouseUp(event);};
5961 util.addEventListener(document, 'mousemove', this.onmousemove);
5962 util.addEventListener(document, 'mouseup', this.onmouseup);
5963 util.preventDefault(event);
5964 };
5965
5966
5967 Slider.prototype.leftToIndex = function (left) {
5968 var width = parseFloat(this.frame.bar.style.width) -
5969 this.frame.slide.clientWidth - 10;
5970 var x = left - 3;
5971
5972 var index = Math.round(x / width * (this.values.length-1));
5973 if (index < 0) index = 0;
5974 if (index > this.values.length-1) index = this.values.length-1;
5975
5976 return index;
5977 };
5978
5979 Slider.prototype.indexToLeft = function (index) {
5980 var width = parseFloat(this.frame.bar.style.width) -
5981 this.frame.slide.clientWidth - 10;
5982
5983 var x = index / (this.values.length-1) * width;
5984 var left = x + 3;
5985
5986 return left;
5987 };
5988
5989
5990
5991 Slider.prototype._onMouseMove = function (event) {
5992 var diff = event.clientX - this.startClientX;
5993 var x = this.startSlideX + diff;
5994
5995 var index = this.leftToIndex(x);
5996
5997 this.setIndex(index);
5998
5999 util.preventDefault();
6000 };
6001
6002
6003 Slider.prototype._onMouseUp = function (event) {
6004 this.frame.style.cursor = 'auto';
6005
6006 // remove event listeners
6007 util.removeEventListener(document, 'mousemove', this.onmousemove);
6008 util.removeEventListener(document, 'mouseup', this.onmouseup);
6009
6010 util.preventDefault();
6011 };
6012
6013 module.exports = Slider;
6014
6015
6016/***/ },
6017/* 11 */
6018/***/ function(module, exports, __webpack_require__) {
6019
6020 /**
6021 * @prototype StepNumber
6022 * The class StepNumber is an iterator for Numbers. You provide a start and end
6023 * value, and a best step size. StepNumber itself rounds to fixed values and
6024 * a finds the step that best fits the provided step.
6025 *
6026 * If prettyStep is true, the step size is chosen as close as possible to the
6027 * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
6028 *
6029 * Example usage:
6030 * var step = new StepNumber(0, 10, 2.5, true);
6031 * step.start();
6032 * while (!step.end()) {
6033 * alert(step.getCurrent());
6034 * step.next();
6035 * }
6036 *
6037 * Version: 1.0
6038 *
6039 * @param {Number} start The start value
6040 * @param {Number} end The end value
6041 * @param {Number} step Optional. Step size. Must be a positive value.
6042 * @param {boolean} prettyStep Optional. If true, the step size is rounded
6043 * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
6044 */
6045 function StepNumber(start, end, step, prettyStep) {
6046 // set default values
6047 this._start = 0;
6048 this._end = 0;
6049 this._step = 1;
6050 this.prettyStep = true;
6051 this.precision = 5;
6052
6053 this._current = 0;
6054 this.setRange(start, end, step, prettyStep);
6055 };
6056
6057 /**
6058 * Set a new range: start, end and step.
6059 *
6060 * @param {Number} start The start value
6061 * @param {Number} end The end value
6062 * @param {Number} step Optional. Step size. Must be a positive value.
6063 * @param {boolean} prettyStep Optional. If true, the step size is rounded
6064 * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
6065 */
6066 StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
6067 this._start = start ? start : 0;
6068 this._end = end ? end : 0;
6069
6070 this.setStep(step, prettyStep);
6071 };
6072
6073 /**
6074 * Set a new step size
6075 * @param {Number} step New step size. Must be a positive value
6076 * @param {boolean} prettyStep Optional. If true, the provided step is rounded
6077 * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
6078 */
6079 StepNumber.prototype.setStep = function(step, prettyStep) {
6080 if (step === undefined || step <= 0)
6081 return;
6082
6083 if (prettyStep !== undefined)
6084 this.prettyStep = prettyStep;
6085
6086 if (this.prettyStep === true)
6087 this._step = StepNumber.calculatePrettyStep(step);
6088 else
6089 this._step = step;
6090 };
6091
6092 /**
6093 * Calculate a nice step size, closest to the desired step size.
6094 * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
6095 * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
6096 * @param {Number} step Desired step size
6097 * @return {Number} Nice step size
6098 */
6099 StepNumber.calculatePrettyStep = function (step) {
6100 var log10 = function (x) {return Math.log(x) / Math.LN10;};
6101
6102 // try three steps (multiple of 1, 2, or 5
6103 var step1 = Math.pow(10, Math.round(log10(step))),
6104 step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
6105 step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
6106
6107 // choose the best step (closest to minimum step)
6108 var prettyStep = step1;
6109 if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
6110 if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
6111
6112 // for safety
6113 if (prettyStep <= 0) {
6114 prettyStep = 1;
6115 }
6116
6117 return prettyStep;
6118 };
6119
6120 /**
6121 * returns the current value of the step
6122 * @return {Number} current value
6123 */
6124 StepNumber.prototype.getCurrent = function () {
6125 return parseFloat(this._current.toPrecision(this.precision));
6126 };
6127
6128 /**
6129 * returns the current step size
6130 * @return {Number} current step size
6131 */
6132 StepNumber.prototype.getStep = function () {
6133 return this._step;
6134 };
6135
6136 /**
6137 * Set the current value to the largest value smaller than start, which
6138 * is a multiple of the step size
6139 */
6140 StepNumber.prototype.start = function() {
6141 this._current = this._start - this._start % this._step;
6142 };
6143
6144 /**
6145 * Do a step, add the step size to the current value
6146 */
6147 StepNumber.prototype.next = function () {
6148 this._current += this._step;
6149 };
6150
6151 /**
6152 * Returns true whether the end is reached
6153 * @return {boolean} True if the current value has passed the end value.
6154 */
6155 StepNumber.prototype.end = function () {
6156 return (this._current > this._end);
6157 };
6158
6159 module.exports = StepNumber;
6160
6161
6162/***/ },
6163/* 12 */
6164/***/ function(module, exports, __webpack_require__) {
6165
6166 var Emitter = __webpack_require__(50);
6167 var Hammer = __webpack_require__(42);
6168 var util = __webpack_require__(1);
6169 var DataSet = __webpack_require__(3);
6170 var DataView = __webpack_require__(4);
6171 var Range = __webpack_require__(15);
6172 var Core = __webpack_require__(44);
6173 var TimeAxis = __webpack_require__(27);
6174 var CurrentTime = __webpack_require__(19);
6175 var CustomTime = __webpack_require__(20);
6176 var ItemSet = __webpack_require__(24);
6177
6178 /**
6179 * Create a timeline visualization
6180 * @param {HTMLElement} container
6181 * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
6182 * @param {Object} [options] See Timeline.setOptions for the available options.
6183 * @constructor
6184 * @extends Core
6185 */
6186 function Timeline (container, items, options) {
6187 if (!(this instanceof Timeline)) {
6188 throw new SyntaxError('Constructor must be called with the new operator');
6189 }
6190
6191 var me = this;
6192 this.defaultOptions = {
6193 start: null,
6194 end: null,
6195
6196 autoResize: true,
6197
6198 orientation: 'bottom',
6199 width: null,
6200 height: null,
6201 maxHeight: null,
6202 minHeight: null
6203 };
6204 this.options = util.deepExtend({}, this.defaultOptions);
6205
6206 // Create the DOM, props, and emitter
6207 this._create(container);
6208
6209 // all components listed here will be repainted automatically
6210 this.components = [];
6211
6212 this.body = {
6213 dom: this.dom,
6214 domProps: this.props,
6215 emitter: {
6216 on: this.on.bind(this),
6217 off: this.off.bind(this),
6218 emit: this.emit.bind(this)
6219 },
6220 util: {
6221 snap: null, // will be specified after TimeAxis is created
6222 toScreen: me._toScreen.bind(me),
6223 toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
6224 toTime: me._toTime.bind(me),
6225 toGlobalTime : me._toGlobalTime.bind(me)
6226 }
6227 };
6228
6229 // range
6230 this.range = new Range(this.body);
6231 this.components.push(this.range);
6232 this.body.range = this.range;
6233
6234 // time axis
6235 this.timeAxis = new TimeAxis(this.body);
6236 this.components.push(this.timeAxis);
6237 this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
6238
6239 // current time bar
6240 this.currentTime = new CurrentTime(this.body);
6241 this.components.push(this.currentTime);
6242
6243 // custom time bar
6244 // Note: time bar will be attached in this.setOptions when selected
6245 this.customTime = new CustomTime(this.body);
6246 this.components.push(this.customTime);
6247
6248 // item set
6249 this.itemSet = new ItemSet(this.body);
6250 this.components.push(this.itemSet);
6251
6252 this.itemsData = null; // DataSet
6253 this.groupsData = null; // DataSet
6254
6255 // apply options
6256 if (options) {
6257 this.setOptions(options);
6258 }
6259
6260 // create itemset
6261 if (items) {
6262 this.setItems(items);
6263 }
6264 else {
6265 this.redraw();
6266 }
6267 }
6268
6269 // Extend the functionality from Core
6270 Timeline.prototype = new Core();
6271
6272 /**
6273 * Set items
6274 * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
6275 */
6276 Timeline.prototype.setItems = function(items) {
6277 var initialLoad = (this.itemsData == null);
6278
6279 // convert to type DataSet when needed
6280 var newDataSet;
6281 if (!items) {
6282 newDataSet = null;
6283 }
6284 else if (items instanceof DataSet || items instanceof DataView) {
6285 newDataSet = items;
6286 }
6287 else {
6288 // turn an array into a dataset
6289 newDataSet = new DataSet(items, {
6290 type: {
6291 start: 'Date',
6292 end: 'Date'
6293 }
6294 });
6295 }
6296
6297 // set items
6298 this.itemsData = newDataSet;
6299 this.itemSet && this.itemSet.setItems(newDataSet);
6300 if (initialLoad) {
6301 if (this.options.start != undefined || this.options.end != undefined) {
6302 var start = this.options.start != undefined ? this.options.start : null;
6303 var end = this.options.end != undefined ? this.options.end : null;
6304
6305 this.setWindow(start, end, {animate: false});
6306 }
6307 else {
6308 this.fit({animate: false});
6309 }
6310 }
6311 };
6312
6313 /**
6314 * Set groups
6315 * @param {vis.DataSet | Array | google.visualization.DataTable} groups
6316 */
6317 Timeline.prototype.setGroups = function(groups) {
6318 // convert to type DataSet when needed
6319 var newDataSet;
6320 if (!groups) {
6321 newDataSet = null;
6322 }
6323 else if (groups instanceof DataSet || groups instanceof DataView) {
6324 newDataSet = groups;
6325 }
6326 else {
6327 // turn an array into a dataset
6328 newDataSet = new DataSet(groups);
6329 }
6330
6331 this.groupsData = newDataSet;
6332 this.itemSet.setGroups(newDataSet);
6333 };
6334
6335 /**
6336 * Set selected items by their id. Replaces the current selection
6337 * Unknown id's are silently ignored.
6338 * @param {string[] | string} [ids] An array with zero or more id's of the items to be
6339 * selected. If ids is an empty array, all items will be
6340 * unselected.
6341 * @param {Object} [options] Available options:
6342 * `focus: boolean`
6343 * If true, focus will be set to the selected item(s)
6344 * `animate: boolean | number`
6345 * If true (default), the range is animated
6346 * smoothly to the new window.
6347 * If a number, the number is taken as duration
6348 * for the animation. Default duration is 500 ms.
6349 * Only applicable when option focus is true.
6350 */
6351 Timeline.prototype.setSelection = function(ids, options) {
6352 this.itemSet && this.itemSet.setSelection(ids);
6353
6354 if (options && options.focus) {
6355 this.focus(ids, options);
6356 }
6357 };
6358
6359 /**
6360 * Get the selected items by their id
6361 * @return {Array} ids The ids of the selected items
6362 */
6363 Timeline.prototype.getSelection = function() {
6364 return this.itemSet && this.itemSet.getSelection() || [];
6365 };
6366
6367 /**
6368 * Adjust the visible window such that the selected item (or multiple items)
6369 * are centered on screen.
6370 * @param {String | String[]} id An item id or array with item ids
6371 * @param {Object} [options] Available options:
6372 * `animate: boolean | number`
6373 * If true (default), the range is animated
6374 * smoothly to the new window.
6375 * If a number, the number is taken as duration
6376 * for the animation. Default duration is 500 ms.
6377 * Only applicable when option focus is true
6378 */
6379 Timeline.prototype.focus = function(id, options) {
6380 if (!this.itemsData || id == undefined) return;
6381
6382 var ids = Array.isArray(id) ? id : [id];
6383
6384 // get the specified item(s)
6385 var itemsData = this.itemsData.getDataSet().get(ids, {
6386 type: {
6387 start: 'Date',
6388 end: 'Date'
6389 }
6390 });
6391
6392 // calculate minimum start and maximum end of specified items
6393 var start = null;
6394 var end = null;
6395 itemsData.forEach(function (itemData) {
6396 var s = itemData.start.valueOf();
6397 var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
6398
6399 if (start === null || s < start) {
6400 start = s;
6401 }
6402
6403 if (end === null || e > end) {
6404 end = e;
6405 }
6406 });
6407
6408 if (start !== null && end !== null) {
6409 // calculate the new middle and interval for the window
6410 var middle = (start + end) / 2;
6411 var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
6412
6413 var animate = (options && options.animate !== undefined) ? options.animate : true;
6414 this.range.setRange(middle - interval / 2, middle + interval / 2, animate);
6415 }
6416 };
6417
6418 /**
6419 * Get the data range of the item set.
6420 * @returns {{min: Date, max: Date}} range A range with a start and end Date.
6421 * When no minimum is found, min==null
6422 * When no maximum is found, max==null
6423 */
6424 Timeline.prototype.getItemRange = function() {
6425 // calculate min from start filed
6426 var dataset = this.itemsData.getDataSet(),
6427 min = null,
6428 max = null;
6429
6430 if (dataset) {
6431 // calculate the minimum value of the field 'start'
6432 var minItem = dataset.min('start');
6433 min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
6434 // Note: we convert first to Date and then to number because else
6435 // a conversion from ISODate to Number will fail
6436
6437 // calculate maximum value of fields 'start' and 'end'
6438 var maxStartItem = dataset.max('start');
6439 if (maxStartItem) {
6440 max = util.convert(maxStartItem.start, 'Date').valueOf();
6441 }
6442 var maxEndItem = dataset.max('end');
6443 if (maxEndItem) {
6444 if (max == null) {
6445 max = util.convert(maxEndItem.end, 'Date').valueOf();
6446 }
6447 else {
6448 max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
6449 }
6450 }
6451 }
6452
6453 return {
6454 min: (min != null) ? new Date(min) : null,
6455 max: (max != null) ? new Date(max) : null
6456 };
6457 };
6458
6459
6460 module.exports = Timeline;
6461
6462
6463/***/ },
6464/* 13 */
6465/***/ function(module, exports, __webpack_require__) {
6466
6467 var Emitter = __webpack_require__(50);
6468 var Hammer = __webpack_require__(42);
6469 var util = __webpack_require__(1);
6470 var DataSet = __webpack_require__(3);
6471 var DataView = __webpack_require__(4);
6472 var Range = __webpack_require__(15);
6473 var Core = __webpack_require__(44);
6474 var TimeAxis = __webpack_require__(27);
6475 var CurrentTime = __webpack_require__(19);
6476 var CustomTime = __webpack_require__(20);
6477 var LineGraph = __webpack_require__(26);
6478
6479 /**
6480 * Create a timeline visualization
6481 * @param {HTMLElement} container
6482 * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
6483 * @param {Object} [options] See Graph2d.setOptions for the available options.
6484 * @constructor
6485 * @extends Core
6486 */
6487 function Graph2d (container, items, options, groups) {
6488 var me = this;
6489 this.defaultOptions = {
6490 start: null,
6491 end: null,
6492
6493 autoResize: true,
6494
6495 orientation: 'bottom',
6496 width: null,
6497 height: null,
6498 maxHeight: null,
6499 minHeight: null
6500 };
6501 this.options = util.deepExtend({}, this.defaultOptions);
6502
6503 // Create the DOM, props, and emitter
6504 this._create(container);
6505
6506 // all components listed here will be repainted automatically
6507 this.components = [];
6508
6509 this.body = {
6510 dom: this.dom,
6511 domProps: this.props,
6512 emitter: {
6513 on: this.on.bind(this),
6514 off: this.off.bind(this),
6515 emit: this.emit.bind(this)
6516 },
6517 util: {
6518 snap: null, // will be specified after TimeAxis is created
6519 toScreen: me._toScreen.bind(me),
6520 toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
6521 toTime: me._toTime.bind(me),
6522 toGlobalTime : me._toGlobalTime.bind(me)
6523 }
6524 };
6525
6526 // range
6527 this.range = new Range(this.body);
6528 this.components.push(this.range);
6529 this.body.range = this.range;
6530
6531 // time axis
6532 this.timeAxis = new TimeAxis(this.body);
6533 this.components.push(this.timeAxis);
6534 this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
6535
6536 // current time bar
6537 this.currentTime = new CurrentTime(this.body);
6538 this.components.push(this.currentTime);
6539
6540 // custom time bar
6541 // Note: time bar will be attached in this.setOptions when selected
6542 this.customTime = new CustomTime(this.body);
6543 this.components.push(this.customTime);
6544
6545 // item set
6546 this.linegraph = new LineGraph(this.body);
6547 this.components.push(this.linegraph);
6548
6549 this.itemsData = null; // DataSet
6550 this.groupsData = null; // DataSet
6551
6552 // apply options
6553 if (options) {
6554 this.setOptions(options);
6555 }
6556
6557 // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
6558 if (groups) {
6559 this.setGroups(groups);
6560 }
6561
6562 // create itemset
6563 if (items) {
6564 this.setItems(items);
6565 }
6566 else {
6567 this.redraw();
6568 }
6569 }
6570
6571 // Extend the functionality from Core
6572 Graph2d.prototype = new Core();
6573
6574 /**
6575 * Set items
6576 * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
6577 */
6578 Graph2d.prototype.setItems = function(items) {
6579 var initialLoad = (this.itemsData == null);
6580
6581 // convert to type DataSet when needed
6582 var newDataSet;
6583 if (!items) {
6584 newDataSet = null;
6585 }
6586 else if (items instanceof DataSet || items instanceof DataView) {
6587 newDataSet = items;
6588 }
6589 else {
6590 // turn an array into a dataset
6591 newDataSet = new DataSet(items, {
6592 type: {
6593 start: 'Date',
6594 end: 'Date'
6595 }
6596 });
6597 }
6598
6599 // set items
6600 this.itemsData = newDataSet;
6601 this.linegraph && this.linegraph.setItems(newDataSet);
6602
6603 if (initialLoad && ('start' in this.options || 'end' in this.options)) {
6604 this.fit();
6605
6606 var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
6607 var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
6608
6609 this.setWindow(start, end);
6610 }
6611 };
6612
6613 /**
6614 * Set groups
6615 * @param {vis.DataSet | Array | google.visualization.DataTable} groups
6616 */
6617 Graph2d.prototype.setGroups = function(groups) {
6618 // convert to type DataSet when needed
6619 var newDataSet;
6620 if (!groups) {
6621 newDataSet = null;
6622 }
6623 else if (groups instanceof DataSet || groups instanceof DataView) {
6624 newDataSet = groups;
6625 }
6626 else {
6627 // turn an array into a dataset
6628 newDataSet = new DataSet(groups);
6629 }
6630
6631 this.groupsData = newDataSet;
6632 this.linegraph.setGroups(newDataSet);
6633 };
6634
6635 /**
6636 * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right).
6637 * @param groupId
6638 * @param width
6639 * @param height
6640 */
6641 Graph2d.prototype.getLegend = function(groupId, width, height) {
6642 if (width === undefined) {width = 15;}
6643 if (height === undefined) {height = 15;}
6644 if (this.linegraph.groups[groupId] !== undefined) {
6645 return this.linegraph.groups[groupId].getLegend(width,height);
6646 }
6647 else {
6648 return "cannot find group:" + groupId;
6649 }
6650 }
6651
6652 /**
6653 * This checks if the visible option of the supplied group (by ID) is true or false.
6654 * @param groupId
6655 * @returns {*}
6656 */
6657 Graph2d.prototype.isGroupVisible = function(groupId) {
6658 if (this.linegraph.groups[groupId] !== undefined) {
6659 return (this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true));
6660 }
6661 else {
6662 return false;
6663 }
6664 }
6665
6666
6667 /**
6668 * Get the data range of the item set.
6669 * @returns {{min: Date, max: Date}} range A range with a start and end Date.
6670 * When no minimum is found, min==null
6671 * When no maximum is found, max==null
6672 */
6673 Graph2d.prototype.getItemRange = function() {
6674 var min = null;
6675 var max = null;
6676
6677 // calculate min from start filed
6678 for (var groupId in this.linegraph.groups) {
6679 if (this.linegraph.groups.hasOwnProperty(groupId)) {
6680 if (this.linegraph.groups[groupId].visible == true) {
6681 for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) {
6682 var item = this.linegraph.groups[groupId].itemsData[i];
6683 var value = util.convert(item.x, 'Date').valueOf();
6684 min = min == null ? value : min > value ? value : min;
6685 max = max == null ? value : max < value ? value : max;
6686 }
6687 }
6688 }
6689 }
6690
6691 return {
6692 min: (min != null) ? new Date(min) : null,
6693 max: (max != null) ? new Date(max) : null
6694 };
6695 };
6696
6697
6698
6699 module.exports = Graph2d;
6700
6701
6702/***/ },
6703/* 14 */
6704/***/ function(module, exports, __webpack_require__) {
6705
6706 /**
6707 * @constructor DataStep
6708 * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
6709 * end data point. The class itself determines the best scale (step size) based on the
6710 * provided start Date, end Date, and minimumStep.
6711 *
6712 * If minimumStep is provided, the step size is chosen as close as possible
6713 * to the minimumStep but larger than minimumStep. If minimumStep is not
6714 * provided, the scale is set to 1 DAY.
6715 * The minimumStep should correspond with the onscreen size of about 6 characters
6716 *
6717 * Alternatively, you can set a scale by hand.
6718 * After creation, you can initialize the class by executing first(). Then you
6719 * can iterate from the start date to the end date via next(). You can check if
6720 * the end date is reached with the function hasNext(). After each step, you can
6721 * retrieve the current date via getCurrent().
6722 * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
6723 * days, to years.
6724 *
6725 * Version: 1.2
6726 *
6727 * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
6728 * or new Date(2010, 9, 21, 23, 45, 00)
6729 * @param {Date} [end] The end date
6730 * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
6731 */
6732 function DataStep(start, end, minimumStep, containerHeight, customRange) {
6733 // variables
6734 this.current = 0;
6735
6736 this.autoScale = true;
6737 this.stepIndex = 0;
6738 this.step = 1;
6739 this.scale = 1;
6740
6741 this.marginStart;
6742 this.marginEnd;
6743 this.deadSpace = 0;
6744
6745 this.majorSteps = [1, 2, 5, 10];
6746 this.minorSteps = [0.25, 0.5, 1, 2];
6747
6748 this.setRange(start, end, minimumStep, containerHeight, customRange);
6749 }
6750
6751
6752
6753 /**
6754 * Set a new range
6755 * If minimumStep is provided, the step size is chosen as close as possible
6756 * to the minimumStep but larger than minimumStep. If minimumStep is not
6757 * provided, the scale is set to 1 DAY.
6758 * The minimumStep should correspond with the onscreen size of about 6 characters
6759 * @param {Number} [start] The start date and time.
6760 * @param {Number} [end] The end date and time.
6761 * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
6762 */
6763 DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
6764 this._start = customRange.min === undefined ? start : customRange.min;
6765 this._end = customRange.max === undefined ? end : customRange.max;
6766
6767 if (this._start == this._end) {
6768 this._start -= 0.75;
6769 this._end += 1;
6770 }
6771
6772 if (this.autoScale) {
6773 this.setMinimumStep(minimumStep, containerHeight);
6774 }
6775 this.setFirst(customRange);
6776 };
6777
6778 /**
6779 * Automatically determine the scale that bests fits the provided minimum step
6780 * @param {Number} [minimumStep] The minimum step size in milliseconds
6781 */
6782 DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
6783 // round to floor
6784 var size = this._end - this._start;
6785 var safeSize = size * 1.2;
6786 var minimumStepValue = minimumStep * (safeSize / containerHeight);
6787 var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
6788
6789 var minorStepIdx = -1;
6790 var magnitudefactor = Math.pow(10,orderOfMagnitude);
6791
6792 var start = 0;
6793 if (orderOfMagnitude < 0) {
6794 start = orderOfMagnitude;
6795 }
6796
6797 var solutionFound = false;
6798 for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
6799 magnitudefactor = Math.pow(10,i);
6800 for (var j = 0; j < this.minorSteps.length; j++) {
6801 var stepSize = magnitudefactor * this.minorSteps[j];
6802 if (stepSize >= minimumStepValue) {
6803 solutionFound = true;
6804 minorStepIdx = j;
6805 break;
6806 }
6807 }
6808 if (solutionFound == true) {
6809 break;
6810 }
6811 }
6812 this.stepIndex = minorStepIdx;
6813 this.scale = magnitudefactor;
6814 this.step = magnitudefactor * this.minorSteps[minorStepIdx];
6815 };
6816
6817
6818
6819 /**
6820 * Round the current date to the first minor date value
6821 * This must be executed once when the current date is set to start Date
6822 */
6823 DataStep.prototype.setFirst = function(customRange) {
6824 if (customRange === undefined) {
6825 customRange = {};
6826 }
6827 var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
6828 var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
6829
6830 this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
6831 this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
6832 this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
6833 this.marginRange = this.marginEnd - this.marginStart;
6834
6835 this.current = this.marginEnd;
6836
6837 };
6838
6839 DataStep.prototype.roundToMinor = function(value) {
6840 var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
6841 if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
6842 return rounded + (this.scale * this.minorSteps[this.stepIndex]);
6843 }
6844 else {
6845 return rounded;
6846 }
6847 }
6848
6849
6850 /**
6851 * Check if the there is a next step
6852 * @return {boolean} true if the current date has not passed the end date
6853 */
6854 DataStep.prototype.hasNext = function () {
6855 return (this.current >= this.marginStart);
6856 };
6857
6858 /**
6859 * Do the next step
6860 */
6861 DataStep.prototype.next = function() {
6862 var prev = this.current;
6863 this.current -= this.step;
6864
6865 // safety mechanism: if current time is still unchanged, move to the end
6866 if (this.current == prev) {
6867 this.current = this._end;
6868 }
6869 };
6870
6871 /**
6872 * Do the next step
6873 */
6874 DataStep.prototype.previous = function() {
6875 this.current += this.step;
6876 this.marginEnd += this.step;
6877 this.marginRange = this.marginEnd - this.marginStart;
6878 };
6879
6880
6881
6882 /**
6883 * Get the current datetime
6884 * @return {String} current The current date
6885 */
6886 DataStep.prototype.getCurrent = function() {
6887 var toPrecision = '' + Number(this.current).toPrecision(5);
6888 if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
6889 for (var i = toPrecision.length-1; i > 0; i--) {
6890 if (toPrecision[i] == "0") {
6891 toPrecision = toPrecision.slice(0,i);
6892 }
6893 else if (toPrecision[i] == "." || toPrecision[i] == ",") {
6894 toPrecision = toPrecision.slice(0,i);
6895 break;
6896 }
6897 else{
6898 break;
6899 }
6900 }
6901 }
6902
6903 return toPrecision;
6904 };
6905
6906
6907
6908 /**
6909 * Snap a date to a rounded value.
6910 * The snap intervals are dependent on the current scale and step.
6911 * @param {Date} date the date to be snapped.
6912 * @return {Date} snappedDate
6913 */
6914 DataStep.prototype.snap = function(date) {
6915
6916 };
6917
6918 /**
6919 * Check if the current value is a major value (for example when the step
6920 * is DAY, a major value is each first day of the MONTH)
6921 * @return {boolean} true if current date is major, else false.
6922 */
6923 DataStep.prototype.isMajor = function() {
6924 return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
6925 };
6926
6927 module.exports = DataStep;
6928
6929
6930/***/ },
6931/* 15 */
6932/***/ function(module, exports, __webpack_require__) {
6933
6934 var util = __webpack_require__(1);
6935 var hammerUtil = __webpack_require__(43);
6936 var moment = __webpack_require__(41);
6937 var Component = __webpack_require__(18);
6938
6939 /**
6940 * @constructor Range
6941 * A Range controls a numeric range with a start and end value.
6942 * The Range adjusts the range based on mouse events or programmatic changes,
6943 * and triggers events when the range is changing or has been changed.
6944 * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
6945 * @param {Object} [options] See description at Range.setOptions
6946 */
6947 function Range(body, options) {
6948 var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
6949 this.start = now.clone().add(-3, 'days').valueOf(); // Number
6950 this.end = now.clone().add(4, 'days').valueOf(); // Number
6951
6952 this.body = body;
6953
6954 // default options
6955 this.defaultOptions = {
6956 start: null,
6957 end: null,
6958 direction: 'horizontal', // 'horizontal' or 'vertical'
6959 moveable: true,
6960 zoomable: true,
6961 min: null,
6962 max: null,
6963 zoomMin: 10, // milliseconds
6964 zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
6965 };
6966 this.options = util.extend({}, this.defaultOptions);
6967
6968 this.props = {
6969 touch: {}
6970 };
6971 this.animateTimer = null;
6972
6973 // drag listeners for dragging
6974 this.body.emitter.on('dragstart', this._onDragStart.bind(this));
6975 this.body.emitter.on('drag', this._onDrag.bind(this));
6976 this.body.emitter.on('dragend', this._onDragEnd.bind(this));
6977
6978 // ignore dragging when holding
6979 this.body.emitter.on('hold', this._onHold.bind(this));
6980
6981 // mouse wheel for zooming
6982 this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
6983 this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
6984
6985 // pinch to zoom
6986 this.body.emitter.on('touch', this._onTouch.bind(this));
6987 this.body.emitter.on('pinch', this._onPinch.bind(this));
6988
6989 this.setOptions(options);
6990 }
6991
6992 Range.prototype = new Component();
6993
6994 /**
6995 * Set options for the range controller
6996 * @param {Object} options Available options:
6997 * {Number | Date | String} start Start date for the range
6998 * {Number | Date | String} end End date for the range
6999 * {Number} min Minimum value for start
7000 * {Number} max Maximum value for end
7001 * {Number} zoomMin Set a minimum value for
7002 * (end - start).
7003 * {Number} zoomMax Set a maximum value for
7004 * (end - start).
7005 * {Boolean} moveable Enable moving of the range
7006 * by dragging. True by default
7007 * {Boolean} zoomable Enable zooming of the range
7008 * by pinching/scrolling. True by default
7009 */
7010 Range.prototype.setOptions = function (options) {
7011 if (options) {
7012 // copy the options that we know
7013 var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate'];
7014 util.selectiveExtend(fields, this.options, options);
7015
7016 if ('start' in options || 'end' in options) {
7017 // apply a new range. both start and end are optional
7018 this.setRange(options.start, options.end);
7019 }
7020 }
7021 };
7022
7023 /**
7024 * Test whether direction has a valid value
7025 * @param {String} direction 'horizontal' or 'vertical'
7026 */
7027 function validateDirection (direction) {
7028 if (direction != 'horizontal' && direction != 'vertical') {
7029 throw new TypeError('Unknown direction "' + direction + '". ' +
7030 'Choose "horizontal" or "vertical".');
7031 }
7032 }
7033
7034 /**
7035 * Set a new start and end range
7036 * @param {Date | Number | String} [start]
7037 * @param {Date | Number | String} [end]
7038 * @param {boolean | number} [animate=false] If true, the range is animated
7039 * smoothly to the new window.
7040 * If animate is a number, the
7041 * number is taken as duration
7042 * Default duration is 500 ms.
7043 *
7044 */
7045 Range.prototype.setRange = function(start, end, animate) {
7046 var _start = start != undefined ? util.convert(start, 'Date').valueOf() : null;
7047 var _end = end != undefined ? util.convert(end, 'Date').valueOf() : null;
7048
7049 this._cancelAnimation();
7050
7051 if (animate) {
7052 var me = this;
7053 var initStart = this.start;
7054 var initEnd = this.end;
7055 var duration = typeof animate === 'number' ? animate : 500;
7056 var initTime = new Date().valueOf();
7057 var anyChanged = false;
7058
7059 function next() {
7060 if (!me.props.touch.dragging) {
7061 var now = new Date().valueOf();
7062 var time = now - initTime;
7063 var done = time > duration;
7064 var s = (done || _start === null) ? _start : util.easeInOutQuad(time, initStart, _start, duration);
7065 var e = (done || _end === null) ? _end : util.easeInOutQuad(time, initEnd, _end, duration);
7066
7067 changed = me._applyRange(s, e);
7068 anyChanged = anyChanged || changed;
7069 if (changed) {
7070 me.body.emitter.emit('rangechange', {start: new Date(me.start), end: new Date(me.end)});
7071 }
7072
7073 if (done) {
7074 if (anyChanged) {
7075 me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end)});
7076 }
7077 }
7078 else {
7079 // animate with as high as possible frame rate, leave 20 ms in between
7080 // each to prevent the browser from blocking
7081 me.animateTimer = setTimeout(next, 20);
7082 }
7083 }
7084 }
7085
7086 return next();
7087 }
7088 else {
7089 var changed = this._applyRange(_start, _end);
7090 if (changed) {
7091 var params = {start: new Date(this.start), end: new Date(this.end)};
7092 this.body.emitter.emit('rangechange', params);
7093 this.body.emitter.emit('rangechanged', params);
7094 }
7095 }
7096 };
7097
7098 /**
7099 * Stop an animation
7100 * @private
7101 */
7102 Range.prototype._cancelAnimation = function () {
7103 if (this.animateTimer) {
7104 clearTimeout(this.animateTimer);
7105 this.animateTimer = null;
7106 }
7107 };
7108
7109 /**
7110 * Set a new start and end range. This method is the same as setRange, but
7111 * does not trigger a range change and range changed event, and it returns
7112 * true when the range is changed
7113 * @param {Number} [start]
7114 * @param {Number} [end]
7115 * @return {Boolean} changed
7116 * @private
7117 */
7118 Range.prototype._applyRange = function(start, end) {
7119 var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
7120 newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
7121 max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
7122 min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
7123 diff;
7124
7125 // check for valid number
7126 if (isNaN(newStart) || newStart === null) {
7127 throw new Error('Invalid start "' + start + '"');
7128 }
7129 if (isNaN(newEnd) || newEnd === null) {
7130 throw new Error('Invalid end "' + end + '"');
7131 }
7132
7133 // prevent start < end
7134 if (newEnd < newStart) {
7135 newEnd = newStart;
7136 }
7137
7138 // prevent start < min
7139 if (min !== null) {
7140 if (newStart < min) {
7141 diff = (min - newStart);
7142 newStart += diff;
7143 newEnd += diff;
7144
7145 // prevent end > max
7146 if (max != null) {
7147 if (newEnd > max) {
7148 newEnd = max;
7149 }
7150 }
7151 }
7152 }
7153
7154 // prevent end > max
7155 if (max !== null) {
7156 if (newEnd > max) {
7157 diff = (newEnd - max);
7158 newStart -= diff;
7159 newEnd -= diff;
7160
7161 // prevent start < min
7162 if (min != null) {
7163 if (newStart < min) {
7164 newStart = min;
7165 }
7166 }
7167 }
7168 }
7169
7170 // prevent (end-start) < zoomMin
7171 if (this.options.zoomMin !== null) {
7172 var zoomMin = parseFloat(this.options.zoomMin);
7173 if (zoomMin < 0) {
7174 zoomMin = 0;
7175 }
7176 if ((newEnd - newStart) < zoomMin) {
7177 if ((this.end - this.start) === zoomMin) {
7178 // ignore this action, we are already zoomed to the minimum
7179 newStart = this.start;
7180 newEnd = this.end;
7181 }
7182 else {
7183 // zoom to the minimum
7184 diff = (zoomMin - (newEnd - newStart));
7185 newStart -= diff / 2;
7186 newEnd += diff / 2;
7187 }
7188 }
7189 }
7190
7191 // prevent (end-start) > zoomMax
7192 if (this.options.zoomMax !== null) {
7193 var zoomMax = parseFloat(this.options.zoomMax);
7194 if (zoomMax < 0) {
7195 zoomMax = 0;
7196 }
7197 if ((newEnd - newStart) > zoomMax) {
7198 if ((this.end - this.start) === zoomMax) {
7199 // ignore this action, we are already zoomed to the maximum
7200 newStart = this.start;
7201 newEnd = this.end;
7202 }
7203 else {
7204 // zoom to the maximum
7205 diff = ((newEnd - newStart) - zoomMax);
7206 newStart += diff / 2;
7207 newEnd -= diff / 2;
7208 }
7209 }
7210 }
7211
7212 var changed = (this.start != newStart || this.end != newEnd);
7213
7214 this.start = newStart;
7215 this.end = newEnd;
7216
7217 return changed;
7218 };
7219
7220 /**
7221 * Retrieve the current range.
7222 * @return {Object} An object with start and end properties
7223 */
7224 Range.prototype.getRange = function() {
7225 return {
7226 start: this.start,
7227 end: this.end
7228 };
7229 };
7230
7231 /**
7232 * Calculate the conversion offset and scale for current range, based on
7233 * the provided width
7234 * @param {Number} width
7235 * @returns {{offset: number, scale: number}} conversion
7236 */
7237 Range.prototype.conversion = function (width) {
7238 return Range.conversion(this.start, this.end, width);
7239 };
7240
7241 /**
7242 * Static method to calculate the conversion offset and scale for a range,
7243 * based on the provided start, end, and width
7244 * @param {Number} start
7245 * @param {Number} end
7246 * @param {Number} width
7247 * @returns {{offset: number, scale: number}} conversion
7248 */
7249 Range.conversion = function (start, end, width) {
7250 if (width != 0 && (end - start != 0)) {
7251 return {
7252 offset: start,
7253 scale: width / (end - start)
7254 }
7255 }
7256 else {
7257 return {
7258 offset: 0,
7259 scale: 1
7260 };
7261 }
7262 };
7263
7264 /**
7265 * Start dragging horizontally or vertically
7266 * @param {Event} event
7267 * @private
7268 */
7269 Range.prototype._onDragStart = function(event) {
7270 // only allow dragging when configured as movable
7271 if (!this.options.moveable) return;
7272
7273 // refuse to drag when we where pinching to prevent the timeline make a jump
7274 // when releasing the fingers in opposite order from the touch screen
7275 if (!this.props.touch.allowDragging) return;
7276
7277 this.props.touch.start = this.start;
7278 this.props.touch.end = this.end;
7279 this.props.touch.dragging = true;
7280
7281 if (this.body.dom.root) {
7282 this.body.dom.root.style.cursor = 'move';
7283 }
7284 };
7285
7286 /**
7287 * Perform dragging operation
7288 * @param {Event} event
7289 * @private
7290 */
7291 Range.prototype._onDrag = function (event) {
7292 // only allow dragging when configured as movable
7293 if (!this.options.moveable) return;
7294 var direction = this.options.direction;
7295 validateDirection(direction);
7296
7297 // refuse to drag when we where pinching to prevent the timeline make a jump
7298 // when releasing the fingers in opposite order from the touch screen
7299 if (!this.props.touch.allowDragging) return;
7300
7301 var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY;
7302 var interval = (this.props.touch.end - this.props.touch.start);
7303 var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
7304 var diffRange = -delta / width * interval;
7305 this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
7306
7307 // fire a rangechange event
7308 this.body.emitter.emit('rangechange', {
7309 start: new Date(this.start),
7310 end: new Date(this.end)
7311 });
7312 };
7313
7314 /**
7315 * Stop dragging operation
7316 * @param {event} event
7317 * @private
7318 */
7319 Range.prototype._onDragEnd = function (event) {
7320 // only allow dragging when configured as movable
7321 if (!this.options.moveable) return;
7322
7323 // refuse to drag when we where pinching to prevent the timeline make a jump
7324 // when releasing the fingers in opposite order from the touch screen
7325 if (!this.props.touch.allowDragging) return;
7326
7327 this.props.touch.dragging = false;
7328 if (this.body.dom.root) {
7329 this.body.dom.root.style.cursor = 'auto';
7330 }
7331
7332 // fire a rangechanged event
7333 this.body.emitter.emit('rangechanged', {
7334 start: new Date(this.start),
7335 end: new Date(this.end)
7336 });
7337 };
7338
7339 /**
7340 * Event handler for mouse wheel event, used to zoom
7341 * Code from http://adomas.org/javascript-mouse-wheel/
7342 * @param {Event} event
7343 * @private
7344 */
7345 Range.prototype._onMouseWheel = function(event) {
7346 // only allow zooming when configured as zoomable and moveable
7347 if (!(this.options.zoomable && this.options.moveable)) return;
7348
7349 // retrieve delta
7350 var delta = 0;
7351 if (event.wheelDelta) { /* IE/Opera. */
7352 delta = event.wheelDelta / 120;
7353 } else if (event.detail) { /* Mozilla case. */
7354 // In Mozilla, sign of delta is different than in IE.
7355 // Also, delta is multiple of 3.
7356 delta = -event.detail / 3;
7357 }
7358
7359 // If delta is nonzero, handle it.
7360 // Basically, delta is now positive if wheel was scrolled up,
7361 // and negative, if wheel was scrolled down.
7362 if (delta) {
7363 // perform the zoom action. Delta is normally 1 or -1
7364
7365 // adjust a negative delta such that zooming in with delta 0.1
7366 // equals zooming out with a delta -0.1
7367 var scale;
7368 if (delta < 0) {
7369 scale = 1 - (delta / 5);
7370 }
7371 else {
7372 scale = 1 / (1 + (delta / 5)) ;
7373 }
7374
7375 // calculate center, the date to zoom around
7376 var gesture = hammerUtil.fakeGesture(this, event),
7377 pointer = getPointer(gesture.center, this.body.dom.center),
7378 pointerDate = this._pointerToDate(pointer);
7379
7380 this.zoom(scale, pointerDate);
7381 }
7382
7383 // Prevent default actions caused by mouse wheel
7384 // (else the page and timeline both zoom and scroll)
7385 event.preventDefault();
7386 };
7387
7388 /**
7389 * Start of a touch gesture
7390 * @private
7391 */
7392 Range.prototype._onTouch = function (event) {
7393 this.props.touch.start = this.start;
7394 this.props.touch.end = this.end;
7395 this.props.touch.allowDragging = true;
7396 this.props.touch.center = null;
7397 };
7398
7399 /**
7400 * On start of a hold gesture
7401 * @private
7402 */
7403 Range.prototype._onHold = function () {
7404 this.props.touch.allowDragging = false;
7405 };
7406
7407 /**
7408 * Handle pinch event
7409 * @param {Event} event
7410 * @private
7411 */
7412 Range.prototype._onPinch = function (event) {
7413 // only allow zooming when configured as zoomable and moveable
7414 if (!(this.options.zoomable && this.options.moveable)) return;
7415
7416 this.props.touch.allowDragging = false;
7417
7418 if (event.gesture.touches.length > 1) {
7419 if (!this.props.touch.center) {
7420 this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
7421 }
7422
7423 var scale = 1 / event.gesture.scale,
7424 initDate = this._pointerToDate(this.props.touch.center);
7425
7426 // calculate new start and end
7427 var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
7428 var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
7429
7430 // apply new range
7431 this.setRange(newStart, newEnd);
7432 }
7433 };
7434
7435 /**
7436 * Helper function to calculate the center date for zooming
7437 * @param {{x: Number, y: Number}} pointer
7438 * @return {number} date
7439 * @private
7440 */
7441 Range.prototype._pointerToDate = function (pointer) {
7442 var conversion;
7443 var direction = this.options.direction;
7444
7445 validateDirection(direction);
7446
7447 if (direction == 'horizontal') {
7448 var width = this.body.domProps.center.width;
7449 conversion = this.conversion(width);
7450 return pointer.x / conversion.scale + conversion.offset;
7451 }
7452 else {
7453 var height = this.body.domProps.center.height;
7454 conversion = this.conversion(height);
7455 return pointer.y / conversion.scale + conversion.offset;
7456 }
7457 };
7458
7459 /**
7460 * Get the pointer location relative to the location of the dom element
7461 * @param {{pageX: Number, pageY: Number}} touch
7462 * @param {Element} element HTML DOM element
7463 * @return {{x: Number, y: Number}} pointer
7464 * @private
7465 */
7466 function getPointer (touch, element) {
7467 return {
7468 x: touch.pageX - util.getAbsoluteLeft(element),
7469 y: touch.pageY - util.getAbsoluteTop(element)
7470 };
7471 }
7472
7473 /**
7474 * Zoom the range the given scale in or out. Start and end date will
7475 * be adjusted, and the timeline will be redrawn. You can optionally give a
7476 * date around which to zoom.
7477 * For example, try scale = 0.9 or 1.1
7478 * @param {Number} scale Scaling factor. Values above 1 will zoom out,
7479 * values below 1 will zoom in.
7480 * @param {Number} [center] Value representing a date around which will
7481 * be zoomed.
7482 */
7483 Range.prototype.zoom = function(scale, center) {
7484 // if centerDate is not provided, take it half between start Date and end Date
7485 if (center == null) {
7486 center = (this.start + this.end) / 2;
7487 }
7488
7489 // calculate new start and end
7490 var newStart = center + (this.start - center) * scale;
7491 var newEnd = center + (this.end - center) * scale;
7492
7493 this.setRange(newStart, newEnd);
7494 };
7495
7496 /**
7497 * Move the range with a given delta to the left or right. Start and end
7498 * value will be adjusted. For example, try delta = 0.1 or -0.1
7499 * @param {Number} delta Moving amount. Positive value will move right,
7500 * negative value will move left
7501 */
7502 Range.prototype.move = function(delta) {
7503 // zoom start Date and end Date relative to the centerDate
7504 var diff = (this.end - this.start);
7505
7506 // apply new values
7507 var newStart = this.start + diff * delta;
7508 var newEnd = this.end + diff * delta;
7509
7510 // TODO: reckon with min and max range
7511
7512 this.start = newStart;
7513 this.end = newEnd;
7514 };
7515
7516 /**
7517 * Move the range to a new center point
7518 * @param {Number} moveTo New center point of the range
7519 */
7520 Range.prototype.moveTo = function(moveTo) {
7521 var center = (this.start + this.end) / 2;
7522
7523 var diff = center - moveTo;
7524
7525 // calculate new start and end
7526 var newStart = this.start - diff;
7527 var newEnd = this.end - diff;
7528
7529 this.setRange(newStart, newEnd);
7530 };
7531
7532 module.exports = Range;
7533
7534
7535/***/ },
7536/* 16 */
7537/***/ function(module, exports, __webpack_require__) {
7538
7539 // Utility functions for ordering and stacking of items
7540 var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
7541
7542 /**
7543 * Order items by their start data
7544 * @param {Item[]} items
7545 */
7546 exports.orderByStart = function(items) {
7547 items.sort(function (a, b) {
7548 return a.data.start - b.data.start;
7549 });
7550 };
7551
7552 /**
7553 * Order items by their end date. If they have no end date, their start date
7554 * is used.
7555 * @param {Item[]} items
7556 */
7557 exports.orderByEnd = function(items) {
7558 items.sort(function (a, b) {
7559 var aTime = ('end' in a.data) ? a.data.end : a.data.start,
7560 bTime = ('end' in b.data) ? b.data.end : b.data.start;
7561
7562 return aTime - bTime;
7563 });
7564 };
7565
7566 /**
7567 * Adjust vertical positions of the items such that they don't overlap each
7568 * other.
7569 * @param {Item[]} items
7570 * All visible items
7571 * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
7572 * Margins between items and between items and the axis.
7573 * @param {boolean} [force=false]
7574 * If true, all items will be repositioned. If false (default), only
7575 * items having a top===null will be re-stacked
7576 */
7577 exports.stack = function(items, margin, force) {
7578 var i, iMax;
7579
7580 if (force) {
7581 // reset top position of all items
7582 for (i = 0, iMax = items.length; i < iMax; i++) {
7583 items[i].top = null;
7584 }
7585 }
7586
7587 // calculate new, non-overlapping positions
7588 for (i = 0, iMax = items.length; i < iMax; i++) {
7589 var item = items[i];
7590 if (item.top === null) {
7591 // initialize top position
7592 item.top = margin.axis;
7593
7594 do {
7595 // TODO: optimize checking for overlap. when there is a gap without items,
7596 // you only need to check for items from the next item on, not from zero
7597 var collidingItem = null;
7598 for (var j = 0, jj = items.length; j < jj; j++) {
7599 var other = items[j];
7600 if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) {
7601 collidingItem = other;
7602 break;
7603 }
7604 }
7605
7606 if (collidingItem != null) {
7607 // There is a collision. Reposition the items above the colliding element
7608 item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
7609 }
7610 } while (collidingItem);
7611 }
7612 }
7613 };
7614
7615 /**
7616 * Adjust vertical positions of the items without stacking them
7617 * @param {Item[]} items
7618 * All visible items
7619 * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
7620 * Margins between items and between items and the axis.
7621 */
7622 exports.nostack = function(items, margin) {
7623 var i, iMax;
7624
7625 // reset top position of all items
7626 for (i = 0, iMax = items.length; i < iMax; i++) {
7627 items[i].top = margin.axis;
7628 }
7629 };
7630
7631 /**
7632 * Test if the two provided items collide
7633 * The items must have parameters left, width, top, and height.
7634 * @param {Item} a The first item
7635 * @param {Item} b The second item
7636 * @param {{horizontal: number, vertical: number}} margin
7637 * An object containing a horizontal and vertical
7638 * minimum required margin.
7639 * @return {boolean} true if a and b collide, else false
7640 */
7641 exports.collision = function(a, b, margin) {
7642 return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
7643 (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
7644 (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
7645 (a.top + a.height + margin.vertical - EPSILON) > b.top);
7646 };
7647
7648
7649/***/ },
7650/* 17 */
7651/***/ function(module, exports, __webpack_require__) {
7652
7653 var moment = __webpack_require__(41);
7654
7655 /**
7656 * @constructor TimeStep
7657 * The class TimeStep is an iterator for dates. You provide a start date and an
7658 * end date. The class itself determines the best scale (step size) based on the
7659 * provided start Date, end Date, and minimumStep.
7660 *
7661 * If minimumStep is provided, the step size is chosen as close as possible
7662 * to the minimumStep but larger than minimumStep. If minimumStep is not
7663 * provided, the scale is set to 1 DAY.
7664 * The minimumStep should correspond with the onscreen size of about 6 characters
7665 *
7666 * Alternatively, you can set a scale by hand.
7667 * After creation, you can initialize the class by executing first(). Then you
7668 * can iterate from the start date to the end date via next(). You can check if
7669 * the end date is reached with the function hasNext(). After each step, you can
7670 * retrieve the current date via getCurrent().
7671 * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
7672 * days, to years.
7673 *
7674 * Version: 1.2
7675 *
7676 * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
7677 * or new Date(2010, 9, 21, 23, 45, 00)
7678 * @param {Date} [end] The end date
7679 * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
7680 */
7681 function TimeStep(start, end, minimumStep) {
7682 // variables
7683 this.current = new Date();
7684 this._start = new Date();
7685 this._end = new Date();
7686
7687 this.autoScale = true;
7688 this.scale = TimeStep.SCALE.DAY;
7689 this.step = 1;
7690
7691 // initialize the range
7692 this.setRange(start, end, minimumStep);
7693 }
7694
7695 /// enum scale
7696 TimeStep.SCALE = {
7697 MILLISECOND: 1,
7698 SECOND: 2,
7699 MINUTE: 3,
7700 HOUR: 4,
7701 DAY: 5,
7702 WEEKDAY: 6,
7703 MONTH: 7,
7704 YEAR: 8
7705 };
7706
7707
7708 /**
7709 * Set a new range
7710 * If minimumStep is provided, the step size is chosen as close as possible
7711 * to the minimumStep but larger than minimumStep. If minimumStep is not
7712 * provided, the scale is set to 1 DAY.
7713 * The minimumStep should correspond with the onscreen size of about 6 characters
7714 * @param {Date} [start] The start date and time.
7715 * @param {Date} [end] The end date and time.
7716 * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
7717 */
7718 TimeStep.prototype.setRange = function(start, end, minimumStep) {
7719 if (!(start instanceof Date) || !(end instanceof Date)) {
7720 throw "No legal start or end date in method setRange";
7721 }
7722
7723 this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
7724 this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
7725
7726 if (this.autoScale) {
7727 this.setMinimumStep(minimumStep);
7728 }
7729 };
7730
7731 /**
7732 * Set the range iterator to the start date.
7733 */
7734 TimeStep.prototype.first = function() {
7735 this.current = new Date(this._start.valueOf());
7736 this.roundToMinor();
7737 };
7738
7739 /**
7740 * Round the current date to the first minor date value
7741 * This must be executed once when the current date is set to start Date
7742 */
7743 TimeStep.prototype.roundToMinor = function() {
7744 // round to floor
7745 // IMPORTANT: we have no breaks in this switch! (this is no bug)
7746 //noinspection FallthroughInSwitchStatementJS
7747 switch (this.scale) {
7748 case TimeStep.SCALE.YEAR:
7749 this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
7750 this.current.setMonth(0);
7751 case TimeStep.SCALE.MONTH: this.current.setDate(1);
7752 case TimeStep.SCALE.DAY: // intentional fall through
7753 case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
7754 case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
7755 case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
7756 case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
7757 //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
7758 }
7759
7760 if (this.step != 1) {
7761 // round down to the first minor value that is a multiple of the current step size
7762 switch (this.scale) {
7763 case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
7764 case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
7765 case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
7766 case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
7767 case TimeStep.SCALE.WEEKDAY: // intentional fall through
7768 case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
7769 case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
7770 case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
7771 default: break;
7772 }
7773 }
7774 };
7775
7776 /**
7777 * Check if the there is a next step
7778 * @return {boolean} true if the current date has not passed the end date
7779 */
7780 TimeStep.prototype.hasNext = function () {
7781 return (this.current.valueOf() <= this._end.valueOf());
7782 };
7783
7784 /**
7785 * Do the next step
7786 */
7787 TimeStep.prototype.next = function() {
7788 var prev = this.current.valueOf();
7789
7790 // Two cases, needed to prevent issues with switching daylight savings
7791 // (end of March and end of October)
7792 if (this.current.getMonth() < 6) {
7793 switch (this.scale) {
7794 case TimeStep.SCALE.MILLISECOND:
7795
7796 this.current = new Date(this.current.valueOf() + this.step); break;
7797 case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
7798 case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
7799 case TimeStep.SCALE.HOUR:
7800 this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
7801 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
7802 var h = this.current.getHours();
7803 this.current.setHours(h - (h % this.step));
7804 break;
7805 case TimeStep.SCALE.WEEKDAY: // intentional fall through
7806 case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
7807 case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
7808 case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
7809 default: break;
7810 }
7811 }
7812 else {
7813 switch (this.scale) {
7814 case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
7815 case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
7816 case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
7817 case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
7818 case TimeStep.SCALE.WEEKDAY: // intentional fall through
7819 case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
7820 case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
7821 case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
7822 default: break;
7823 }
7824 }
7825
7826 if (this.step != 1) {
7827 // round down to the correct major value
7828 switch (this.scale) {
7829 case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
7830 case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
7831 case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
7832 case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
7833 case TimeStep.SCALE.WEEKDAY: // intentional fall through
7834 case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
7835 case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
7836 case TimeStep.SCALE.YEAR: break; // nothing to do for year
7837 default: break;
7838 }
7839 }
7840
7841 // safety mechanism: if current time is still unchanged, move to the end
7842 if (this.current.valueOf() == prev) {
7843 this.current = new Date(this._end.valueOf());
7844 }
7845 };
7846
7847
7848 /**
7849 * Get the current datetime
7850 * @return {Date} current The current date
7851 */
7852 TimeStep.prototype.getCurrent = function() {
7853 return this.current;
7854 };
7855
7856 /**
7857 * Set a custom scale. Autoscaling will be disabled.
7858 * For example setScale(SCALE.MINUTES, 5) will result
7859 * in minor steps of 5 minutes, and major steps of an hour.
7860 *
7861 * @param {TimeStep.SCALE} newScale
7862 * A scale. Choose from SCALE.MILLISECOND,
7863 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
7864 * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
7865 * SCALE.YEAR.
7866 * @param {Number} newStep A step size, by default 1. Choose for
7867 * example 1, 2, 5, or 10.
7868 */
7869 TimeStep.prototype.setScale = function(newScale, newStep) {
7870 this.scale = newScale;
7871
7872 if (newStep > 0) {
7873 this.step = newStep;
7874 }
7875
7876 this.autoScale = false;
7877 };
7878
7879 /**
7880 * Enable or disable autoscaling
7881 * @param {boolean} enable If true, autoascaling is set true
7882 */
7883 TimeStep.prototype.setAutoScale = function (enable) {
7884 this.autoScale = enable;
7885 };
7886
7887
7888 /**
7889 * Automatically determine the scale that bests fits the provided minimum step
7890 * @param {Number} [minimumStep] The minimum step size in milliseconds
7891 */
7892 TimeStep.prototype.setMinimumStep = function(minimumStep) {
7893 if (minimumStep == undefined) {
7894 return;
7895 }
7896
7897 var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
7898 var stepMonth = (1000 * 60 * 60 * 24 * 30);
7899 var stepDay = (1000 * 60 * 60 * 24);
7900 var stepHour = (1000 * 60 * 60);
7901 var stepMinute = (1000 * 60);
7902 var stepSecond = (1000);
7903 var stepMillisecond= (1);
7904
7905 // find the smallest step that is larger than the provided minimumStep
7906 if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
7907 if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
7908 if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
7909 if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
7910 if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
7911 if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
7912 if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
7913 if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
7914 if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
7915 if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
7916 if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
7917 if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
7918 if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
7919 if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
7920 if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
7921 if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
7922 if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
7923 if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
7924 if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
7925 if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
7926 if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
7927 if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
7928 if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
7929 if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
7930 if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
7931 if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
7932 if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
7933 if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
7934 if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
7935 };
7936
7937 /**
7938 * Snap a date to a rounded value.
7939 * The snap intervals are dependent on the current scale and step.
7940 * @param {Date} date the date to be snapped.
7941 * @return {Date} snappedDate
7942 */
7943 TimeStep.prototype.snap = function(date) {
7944 var clone = new Date(date.valueOf());
7945
7946 if (this.scale == TimeStep.SCALE.YEAR) {
7947 var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
7948 clone.setFullYear(Math.round(year / this.step) * this.step);
7949 clone.setMonth(0);
7950 clone.setDate(0);
7951 clone.setHours(0);
7952 clone.setMinutes(0);
7953 clone.setSeconds(0);
7954 clone.setMilliseconds(0);
7955 }
7956 else if (this.scale == TimeStep.SCALE.MONTH) {
7957 if (clone.getDate() > 15) {
7958 clone.setDate(1);
7959 clone.setMonth(clone.getMonth() + 1);
7960 // important: first set Date to 1, after that change the month.
7961 }
7962 else {
7963 clone.setDate(1);
7964 }
7965
7966 clone.setHours(0);
7967 clone.setMinutes(0);
7968 clone.setSeconds(0);
7969 clone.setMilliseconds(0);
7970 }
7971 else if (this.scale == TimeStep.SCALE.DAY) {
7972 //noinspection FallthroughInSwitchStatementJS
7973 switch (this.step) {
7974 case 5:
7975 case 2:
7976 clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
7977 default:
7978 clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
7979 }
7980 clone.setMinutes(0);
7981 clone.setSeconds(0);
7982 clone.setMilliseconds(0);
7983 }
7984 else if (this.scale == TimeStep.SCALE.WEEKDAY) {
7985 //noinspection FallthroughInSwitchStatementJS
7986 switch (this.step) {
7987 case 5:
7988 case 2:
7989 clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
7990 default:
7991 clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
7992 }
7993 clone.setMinutes(0);
7994 clone.setSeconds(0);
7995 clone.setMilliseconds(0);
7996 }
7997 else if (this.scale == TimeStep.SCALE.HOUR) {
7998 switch (this.step) {
7999 case 4:
8000 clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
8001 default:
8002 clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
8003 }
8004 clone.setSeconds(0);
8005 clone.setMilliseconds(0);
8006 } else if (this.scale == TimeStep.SCALE.MINUTE) {
8007 //noinspection FallthroughInSwitchStatementJS
8008 switch (this.step) {
8009 case 15:
8010 case 10:
8011 clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
8012 clone.setSeconds(0);
8013 break;
8014 case 5:
8015 clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
8016 default:
8017 clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
8018 }
8019 clone.setMilliseconds(0);
8020 }
8021 else if (this.scale == TimeStep.SCALE.SECOND) {
8022 //noinspection FallthroughInSwitchStatementJS
8023 switch (this.step) {
8024 case 15:
8025 case 10:
8026 clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
8027 clone.setMilliseconds(0);
8028 break;
8029 case 5:
8030 clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
8031 default:
8032 clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
8033 }
8034 }
8035 else if (this.scale == TimeStep.SCALE.MILLISECOND) {
8036 var step = this.step > 5 ? this.step / 2 : 1;
8037 clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
8038 }
8039
8040 return clone;
8041 };
8042
8043 /**
8044 * Check if the current value is a major value (for example when the step
8045 * is DAY, a major value is each first day of the MONTH)
8046 * @return {boolean} true if current date is major, else false.
8047 */
8048 TimeStep.prototype.isMajor = function() {
8049 switch (this.scale) {
8050 case TimeStep.SCALE.MILLISECOND:
8051 return (this.current.getMilliseconds() == 0);
8052 case TimeStep.SCALE.SECOND:
8053 return (this.current.getSeconds() == 0);
8054 case TimeStep.SCALE.MINUTE:
8055 return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
8056 // Note: this is no bug. Major label is equal for both minute and hour scale
8057 case TimeStep.SCALE.HOUR:
8058 return (this.current.getHours() == 0);
8059 case TimeStep.SCALE.WEEKDAY: // intentional fall through
8060 case TimeStep.SCALE.DAY:
8061 return (this.current.getDate() == 1);
8062 case TimeStep.SCALE.MONTH:
8063 return (this.current.getMonth() == 0);
8064 case TimeStep.SCALE.YEAR:
8065 return false;
8066 default:
8067 return false;
8068 }
8069 };
8070
8071
8072 /**
8073 * Returns formatted text for the minor axislabel, depending on the current
8074 * date and the scale. For example when scale is MINUTE, the current time is
8075 * formatted as "hh:mm".
8076 * @param {Date} [date] custom date. if not provided, current date is taken
8077 */
8078 TimeStep.prototype.getLabelMinor = function(date) {
8079 if (date == undefined) {
8080 date = this.current;
8081 }
8082
8083 switch (this.scale) {
8084 case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
8085 case TimeStep.SCALE.SECOND: return moment(date).format('s');
8086 case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
8087 case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
8088 case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
8089 case TimeStep.SCALE.DAY: return moment(date).format('D');
8090 case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
8091 case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
8092 default: return '';
8093 }
8094 };
8095
8096
8097 /**
8098 * Returns formatted text for the major axis label, depending on the current
8099 * date and the scale. For example when scale is MINUTE, the major scale is
8100 * hours, and the hour will be formatted as "hh".
8101 * @param {Date} [date] custom date. if not provided, current date is taken
8102 */
8103 TimeStep.prototype.getLabelMajor = function(date) {
8104 if (date == undefined) {
8105 date = this.current;
8106 }
8107
8108 //noinspection FallthroughInSwitchStatementJS
8109 switch (this.scale) {
8110 case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
8111 case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
8112 case TimeStep.SCALE.MINUTE:
8113 case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
8114 case TimeStep.SCALE.WEEKDAY:
8115 case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
8116 case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
8117 case TimeStep.SCALE.YEAR: return '';
8118 default: return '';
8119 }
8120 };
8121
8122 module.exports = TimeStep;
8123
8124
8125/***/ },
8126/* 18 */
8127/***/ function(module, exports, __webpack_require__) {
8128
8129 /**
8130 * Prototype for visual components
8131 * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
8132 * @param {Object} [options]
8133 */
8134 function Component (body, options) {
8135 this.options = null;
8136 this.props = null;
8137 }
8138
8139 /**
8140 * Set options for the component. The new options will be merged into the
8141 * current options.
8142 * @param {Object} options
8143 */
8144 Component.prototype.setOptions = function(options) {
8145 if (options) {
8146 util.extend(this.options, options);
8147 }
8148 };
8149
8150 /**
8151 * Repaint the component
8152 * @return {boolean} Returns true if the component is resized
8153 */
8154 Component.prototype.redraw = function() {
8155 // should be implemented by the component
8156 return false;
8157 };
8158
8159 /**
8160 * Destroy the component. Cleanup DOM and event listeners
8161 */
8162 Component.prototype.destroy = function() {
8163 // should be implemented by the component
8164 };
8165
8166 /**
8167 * Test whether the component is resized since the last time _isResized() was
8168 * called.
8169 * @return {Boolean} Returns true if the component is resized
8170 * @protected
8171 */
8172 Component.prototype._isResized = function() {
8173 var resized = (this.props._previousWidth !== this.props.width ||
8174 this.props._previousHeight !== this.props.height);
8175
8176 this.props._previousWidth = this.props.width;
8177 this.props._previousHeight = this.props.height;
8178
8179 return resized;
8180 };
8181
8182 module.exports = Component;
8183
8184
8185/***/ },
8186/* 19 */
8187/***/ function(module, exports, __webpack_require__) {
8188
8189 var util = __webpack_require__(1);
8190 var Component = __webpack_require__(18);
8191 var moment = __webpack_require__(41);
8192 var locales = __webpack_require__(45);
8193
8194 /**
8195 * A current time bar
8196 * @param {{range: Range, dom: Object, domProps: Object}} body
8197 * @param {Object} [options] Available parameters:
8198 * {Boolean} [showCurrentTime]
8199 * @constructor CurrentTime
8200 * @extends Component
8201 */
8202 function CurrentTime (body, options) {
8203 this.body = body;
8204
8205 // default options
8206 this.defaultOptions = {
8207 showCurrentTime: true,
8208
8209 locales: locales,
8210 locale: 'en'
8211 };
8212 this.options = util.extend({}, this.defaultOptions);
8213 this.offset = 0;
8214
8215 this._create();
8216
8217 this.setOptions(options);
8218 }
8219
8220 CurrentTime.prototype = new Component();
8221
8222 /**
8223 * Create the HTML DOM for the current time bar
8224 * @private
8225 */
8226 CurrentTime.prototype._create = function() {
8227 var bar = document.createElement('div');
8228 bar.className = 'currenttime';
8229 bar.style.position = 'absolute';
8230 bar.style.top = '0px';
8231 bar.style.height = '100%';
8232
8233 this.bar = bar;
8234 };
8235
8236 /**
8237 * Destroy the CurrentTime bar
8238 */
8239 CurrentTime.prototype.destroy = function () {
8240 this.options.showCurrentTime = false;
8241 this.redraw(); // will remove the bar from the DOM and stop refreshing
8242
8243 this.body = null;
8244 };
8245
8246 /**
8247 * Set options for the component. Options will be merged in current options.
8248 * @param {Object} options Available parameters:
8249 * {boolean} [showCurrentTime]
8250 */
8251 CurrentTime.prototype.setOptions = function(options) {
8252 if (options) {
8253 // copy all options that we know
8254 util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options);
8255 }
8256 };
8257
8258 /**
8259 * Repaint the component
8260 * @return {boolean} Returns true if the component is resized
8261 */
8262 CurrentTime.prototype.redraw = function() {
8263 if (this.options.showCurrentTime) {
8264 var parent = this.body.dom.backgroundVertical;
8265 if (this.bar.parentNode != parent) {
8266 // attach to the dom
8267 if (this.bar.parentNode) {
8268 this.bar.parentNode.removeChild(this.bar);
8269 }
8270 parent.appendChild(this.bar);
8271
8272 this.start();
8273 }
8274
8275 var now = new Date(new Date().valueOf() + this.offset);
8276 var x = this.body.util.toScreen(now);
8277
8278 var locale = this.options.locales[this.options.locale];
8279 var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss');
8280 title = title.charAt(0).toUpperCase() + title.substring(1);
8281
8282 this.bar.style.left = x + 'px';
8283 this.bar.title = title;
8284 }
8285 else {
8286 // remove the line from the DOM
8287 if (this.bar.parentNode) {
8288 this.bar.parentNode.removeChild(this.bar);
8289 }
8290 this.stop();
8291 }
8292
8293 return false;
8294 };
8295
8296 /**
8297 * Start auto refreshing the current time bar
8298 */
8299 CurrentTime.prototype.start = function() {
8300 var me = this;
8301
8302 function update () {
8303 me.stop();
8304
8305 // determine interval to refresh
8306 var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
8307 var interval = 1 / scale / 10;
8308 if (interval < 30) interval = 30;
8309 if (interval > 1000) interval = 1000;
8310
8311 me.redraw();
8312
8313 // start a timer to adjust for the new time
8314 me.currentTimeTimer = setTimeout(update, interval);
8315 }
8316
8317 update();
8318 };
8319
8320 /**
8321 * Stop auto refreshing the current time bar
8322 */
8323 CurrentTime.prototype.stop = function() {
8324 if (this.currentTimeTimer !== undefined) {
8325 clearTimeout(this.currentTimeTimer);
8326 delete this.currentTimeTimer;
8327 }
8328 };
8329
8330 /**
8331 * Set a current time. This can be used for example to ensure that a client's
8332 * time is synchronized with a shared server time.
8333 * @param {Date | String | Number} time A Date, unix timestamp, or
8334 * ISO date string.
8335 */
8336 CurrentTime.prototype.setCurrentTime = function(time) {
8337 var t = util.convert(time, 'Date').valueOf();
8338 var now = new Date().valueOf();
8339 this.offset = t - now;
8340 this.redraw();
8341 };
8342
8343 /**
8344 * Get the current time.
8345 * @return {Date} Returns the current time.
8346 */
8347 CurrentTime.prototype.getCurrentTime = function() {
8348 return new Date(new Date().valueOf() + this.offset);
8349 };
8350
8351 module.exports = CurrentTime;
8352
8353
8354/***/ },
8355/* 20 */
8356/***/ function(module, exports, __webpack_require__) {
8357
8358 var Hammer = __webpack_require__(42);
8359 var util = __webpack_require__(1);
8360 var Component = __webpack_require__(18);
8361 var moment = __webpack_require__(41);
8362 var locales = __webpack_require__(45);
8363
8364 /**
8365 * A custom time bar
8366 * @param {{range: Range, dom: Object}} body
8367 * @param {Object} [options] Available parameters:
8368 * {Boolean} [showCustomTime]
8369 * @constructor CustomTime
8370 * @extends Component
8371 */
8372
8373 function CustomTime (body, options) {
8374 this.body = body;
8375
8376 // default options
8377 this.defaultOptions = {
8378 showCustomTime: false,
8379 locales: locales,
8380 locale: 'en'
8381 };
8382 this.options = util.extend({}, this.defaultOptions);
8383
8384 this.customTime = new Date();
8385 this.eventParams = {}; // stores state parameters while dragging the bar
8386
8387 // create the DOM
8388 this._create();
8389
8390 this.setOptions(options);
8391 }
8392
8393 CustomTime.prototype = new Component();
8394
8395 /**
8396 * Set options for the component. Options will be merged in current options.
8397 * @param {Object} options Available parameters:
8398 * {boolean} [showCustomTime]
8399 */
8400 CustomTime.prototype.setOptions = function(options) {
8401 if (options) {
8402 // copy all options that we know
8403 util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options);
8404 }
8405 };
8406
8407 /**
8408 * Create the DOM for the custom time
8409 * @private
8410 */
8411 CustomTime.prototype._create = function() {
8412 var bar = document.createElement('div');
8413 bar.className = 'customtime';
8414 bar.style.position = 'absolute';
8415 bar.style.top = '0px';
8416 bar.style.height = '100%';
8417 this.bar = bar;
8418
8419 var drag = document.createElement('div');
8420 drag.style.position = 'relative';
8421 drag.style.top = '0px';
8422 drag.style.left = '-10px';
8423 drag.style.height = '100%';
8424 drag.style.width = '20px';
8425 bar.appendChild(drag);
8426
8427 // attach event listeners
8428 this.hammer = Hammer(bar, {
8429 prevent_default: true
8430 });
8431 this.hammer.on('dragstart', this._onDragStart.bind(this));
8432 this.hammer.on('drag', this._onDrag.bind(this));
8433 this.hammer.on('dragend', this._onDragEnd.bind(this));
8434 };
8435
8436 /**
8437 * Destroy the CustomTime bar
8438 */
8439 CustomTime.prototype.destroy = function () {
8440 this.options.showCustomTime = false;
8441 this.redraw(); // will remove the bar from the DOM
8442
8443 this.hammer.enable(false);
8444 this.hammer = null;
8445
8446 this.body = null;
8447 };
8448
8449 /**
8450 * Repaint the component
8451 * @return {boolean} Returns true if the component is resized
8452 */
8453 CustomTime.prototype.redraw = function () {
8454 if (this.options.showCustomTime) {
8455 var parent = this.body.dom.backgroundVertical;
8456 if (this.bar.parentNode != parent) {
8457 // attach to the dom
8458 if (this.bar.parentNode) {
8459 this.bar.parentNode.removeChild(this.bar);
8460 }
8461 parent.appendChild(this.bar);
8462 }
8463
8464 var x = this.body.util.toScreen(this.customTime);
8465
8466 var locale = this.options.locales[this.options.locale];
8467 var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss');
8468 title = title.charAt(0).toUpperCase() + title.substring(1);
8469
8470 this.bar.style.left = x + 'px';
8471 this.bar.title = title;
8472 }
8473 else {
8474 // remove the line from the DOM
8475 if (this.bar.parentNode) {
8476 this.bar.parentNode.removeChild(this.bar);
8477 }
8478 }
8479
8480 return false;
8481 };
8482
8483 /**
8484 * Set custom time.
8485 * @param {Date | number | string} time
8486 */
8487 CustomTime.prototype.setCustomTime = function(time) {
8488 this.customTime = util.convert(time, 'Date');
8489 this.redraw();
8490 };
8491
8492 /**
8493 * Retrieve the current custom time.
8494 * @return {Date} customTime
8495 */
8496 CustomTime.prototype.getCustomTime = function() {
8497 return new Date(this.customTime.valueOf());
8498 };
8499
8500 /**
8501 * Start moving horizontally
8502 * @param {Event} event
8503 * @private
8504 */
8505 CustomTime.prototype._onDragStart = function(event) {
8506 this.eventParams.dragging = true;
8507 this.eventParams.customTime = this.customTime;
8508
8509 event.stopPropagation();
8510 event.preventDefault();
8511 };
8512
8513 /**
8514 * Perform moving operating.
8515 * @param {Event} event
8516 * @private
8517 */
8518 CustomTime.prototype._onDrag = function (event) {
8519 if (!this.eventParams.dragging) return;
8520
8521 var deltaX = event.gesture.deltaX,
8522 x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
8523 time = this.body.util.toTime(x);
8524
8525 this.setCustomTime(time);
8526
8527 // fire a timechange event
8528 this.body.emitter.emit('timechange', {
8529 time: new Date(this.customTime.valueOf())
8530 });
8531
8532 event.stopPropagation();
8533 event.preventDefault();
8534 };
8535
8536 /**
8537 * Stop moving operating.
8538 * @param {event} event
8539 * @private
8540 */
8541 CustomTime.prototype._onDragEnd = function (event) {
8542 if (!this.eventParams.dragging) return;
8543
8544 // fire a timechanged event
8545 this.body.emitter.emit('timechanged', {
8546 time: new Date(this.customTime.valueOf())
8547 });
8548
8549 event.stopPropagation();
8550 event.preventDefault();
8551 };
8552
8553 module.exports = CustomTime;
8554
8555
8556/***/ },
8557/* 21 */
8558/***/ function(module, exports, __webpack_require__) {
8559
8560 var util = __webpack_require__(1);
8561 var DOMutil = __webpack_require__(2);
8562 var Component = __webpack_require__(18);
8563 var DataStep = __webpack_require__(14);
8564
8565 /**
8566 * A horizontal time axis
8567 * @param {Object} [options] See DataAxis.setOptions for the available
8568 * options.
8569 * @constructor DataAxis
8570 * @extends Component
8571 * @param body
8572 */
8573 function DataAxis (body, options, svg, linegraphOptions) {
8574 this.id = util.randomUUID();
8575 this.body = body;
8576
8577 this.defaultOptions = {
8578 orientation: 'left', // supported: 'left', 'right'
8579 showMinorLabels: true,
8580 showMajorLabels: true,
8581 icons: true,
8582 majorLinesOffset: 7,
8583 minorLinesOffset: 4,
8584 labelOffsetX: 10,
8585 labelOffsetY: 2,
8586 iconWidth: 20,
8587 width: '40px',
8588 visible: true,
8589 customRange: {
8590 left: {min:undefined, max:undefined},
8591 right: {min:undefined, max:undefined}
8592 }
8593 };
8594
8595 this.linegraphOptions = linegraphOptions;
8596 this.linegraphSVG = svg;
8597 this.props = {};
8598 this.DOMelements = { // dynamic elements
8599 lines: {},
8600 labels: {}
8601 };
8602
8603 this.dom = {};
8604
8605 this.range = {start:0, end:0};
8606
8607 this.options = util.extend({}, this.defaultOptions);
8608 this.conversionFactor = 1;
8609
8610 this.setOptions(options);
8611 this.width = Number(('' + this.options.width).replace("px",""));
8612 this.minWidth = this.width;
8613 this.height = this.linegraphSVG.offsetHeight;
8614
8615 this.stepPixels = 25;
8616 this.stepPixelsForced = 25;
8617 this.lineOffset = 0;
8618 this.master = true;
8619 this.svgElements = {};
8620
8621
8622 this.groups = {};
8623 this.amountOfGroups = 0;
8624
8625 // create the HTML DOM
8626 this._create();
8627 }
8628
8629 DataAxis.prototype = new Component();
8630
8631
8632
8633 DataAxis.prototype.addGroup = function(label, graphOptions) {
8634 if (!this.groups.hasOwnProperty(label)) {
8635 this.groups[label] = graphOptions;
8636 }
8637 this.amountOfGroups += 1;
8638 };
8639
8640 DataAxis.prototype.updateGroup = function(label, graphOptions) {
8641 this.groups[label] = graphOptions;
8642 };
8643
8644 DataAxis.prototype.removeGroup = function(label) {
8645 if (this.groups.hasOwnProperty(label)) {
8646 delete this.groups[label];
8647 this.amountOfGroups -= 1;
8648 }
8649 };
8650
8651
8652 DataAxis.prototype.setOptions = function (options) {
8653 if (options) {
8654 var redraw = false;
8655 if (this.options.orientation != options.orientation && options.orientation !== undefined) {
8656 redraw = true;
8657 }
8658 var fields = [
8659 'orientation',
8660 'showMinorLabels',
8661 'showMajorLabels',
8662 'icons',
8663 'majorLinesOffset',
8664 'minorLinesOffset',
8665 'labelOffsetX',
8666 'labelOffsetY',
8667 'iconWidth',
8668 'width',
8669 'visible',
8670 'customRange'
8671 ];
8672 util.selectiveExtend(fields, this.options, options);
8673
8674 this.minWidth = Number(('' + this.options.width).replace("px",""));
8675
8676 if (redraw == true && this.dom.frame) {
8677 this.hide();
8678 this.show();
8679 }
8680 }
8681 };
8682
8683
8684 /**
8685 * Create the HTML DOM for the DataAxis
8686 */
8687 DataAxis.prototype._create = function() {
8688 this.dom.frame = document.createElement('div');
8689 this.dom.frame.style.width = this.options.width;
8690 this.dom.frame.style.height = this.height;
8691
8692 this.dom.lineContainer = document.createElement('div');
8693 this.dom.lineContainer.style.width = '100%';
8694 this.dom.lineContainer.style.height = this.height;
8695
8696 // create svg element for graph drawing.
8697 this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
8698 this.svg.style.position = "absolute";
8699 this.svg.style.top = '0px';
8700 this.svg.style.height = '100%';
8701 this.svg.style.width = '100%';
8702 this.svg.style.display = "block";
8703 this.dom.frame.appendChild(this.svg);
8704 };
8705
8706 DataAxis.prototype._redrawGroupIcons = function () {
8707 DOMutil.prepareElements(this.svgElements);
8708
8709 var x;
8710 var iconWidth = this.options.iconWidth;
8711 var iconHeight = 15;
8712 var iconOffset = 4;
8713 var y = iconOffset + 0.5 * iconHeight;
8714
8715 if (this.options.orientation == 'left') {
8716 x = iconOffset;
8717 }
8718 else {
8719 x = this.width - iconWidth - iconOffset;
8720 }
8721
8722 for (var groupId in this.groups) {
8723 if (this.groups.hasOwnProperty(groupId)) {
8724 if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
8725 this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
8726 y += iconHeight + iconOffset;
8727 }
8728 }
8729 }
8730
8731 DOMutil.cleanupElements(this.svgElements);
8732 };
8733
8734 /**
8735 * Create the HTML DOM for the DataAxis
8736 */
8737 DataAxis.prototype.show = function() {
8738 if (!this.dom.frame.parentNode) {
8739 if (this.options.orientation == 'left') {
8740 this.body.dom.left.appendChild(this.dom.frame);
8741 }
8742 else {
8743 this.body.dom.right.appendChild(this.dom.frame);
8744 }
8745 }
8746
8747 if (!this.dom.lineContainer.parentNode) {
8748 this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
8749 }
8750 };
8751
8752 /**
8753 * Create the HTML DOM for the DataAxis
8754 */
8755 DataAxis.prototype.hide = function() {
8756 if (this.dom.frame.parentNode) {
8757 this.dom.frame.parentNode.removeChild(this.dom.frame);
8758 }
8759
8760 if (this.dom.lineContainer.parentNode) {
8761 this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
8762 }
8763 };
8764
8765 /**
8766 * Set a range (start and end)
8767 * @param end
8768 * @param start
8769 * @param end
8770 */
8771 DataAxis.prototype.setRange = function (start, end) {
8772 this.range.start = start;
8773 this.range.end = end;
8774 };
8775
8776 /**
8777 * Repaint the component
8778 * @return {boolean} Returns true if the component is resized
8779 */
8780 DataAxis.prototype.redraw = function () {
8781 var changeCalled = false;
8782 var activeGroups = 0;
8783 for (var groupId in this.groups) {
8784 if (this.groups.hasOwnProperty(groupId)) {
8785 if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
8786 activeGroups++;
8787 }
8788 }
8789 }
8790 if (this.amountOfGroups == 0 || activeGroups == 0) {
8791 this.hide();
8792 }
8793 else {
8794 this.show();
8795 this.height = Number(this.linegraphSVG.style.height.replace("px",""));
8796 // svg offsetheight did not work in firefox and explorer...
8797
8798 this.dom.lineContainer.style.height = this.height + 'px';
8799 this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
8800
8801 var props = this.props;
8802 var frame = this.dom.frame;
8803
8804 // update classname
8805 frame.className = 'dataaxis';
8806
8807 // calculate character width and height
8808 this._calculateCharSize();
8809
8810 var orientation = this.options.orientation;
8811 var showMinorLabels = this.options.showMinorLabels;
8812 var showMajorLabels = this.options.showMajorLabels;
8813
8814 // determine the width and height of the elemens for the axis
8815 props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
8816 props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
8817
8818 props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
8819 props.minorLineHeight = 1;
8820 props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
8821 props.majorLineHeight = 1;
8822
8823 // take frame offline while updating (is almost twice as fast)
8824 if (orientation == 'left') {
8825 frame.style.top = '0';
8826 frame.style.left = '0';
8827 frame.style.bottom = '';
8828 frame.style.width = this.width + 'px';
8829 frame.style.height = this.height + "px";
8830 }
8831 else { // right
8832 frame.style.top = '';
8833 frame.style.bottom = '0';
8834 frame.style.left = '0';
8835 frame.style.width = this.width + 'px';
8836 frame.style.height = this.height + "px";
8837 }
8838 changeCalled = this._redrawLabels();
8839 if (this.options.icons == true) {
8840 this._redrawGroupIcons();
8841 }
8842 }
8843 return changeCalled;
8844 };
8845
8846 /**
8847 * Repaint major and minor text labels and vertical grid lines
8848 * @private
8849 */
8850 DataAxis.prototype._redrawLabels = function () {
8851 DOMutil.prepareElements(this.DOMelements.lines);
8852 DOMutil.prepareElements(this.DOMelements.labels);
8853
8854 var orientation = this.options['orientation'];
8855
8856 // calculate range and step (step such that we have space for 7 characters per label)
8857 var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
8858
8859 var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation]);
8860 this.step = step;
8861 // get the distance in pixels for a step
8862 // dead space is space that is "left over" after a step
8863 var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step));
8864 this.stepPixels = stepPixels;
8865
8866 var amountOfSteps = this.height / stepPixels;
8867 var stepDifference = 0;
8868
8869 if (this.master == false) {
8870 stepPixels = this.stepPixelsForced;
8871 stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps);
8872 for (var i = 0; i < 0.5 * stepDifference; i++) {
8873 step.previous();
8874 }
8875 amountOfSteps = this.height / stepPixels;
8876 }
8877 else {
8878 amountOfSteps += 0.25;
8879 }
8880
8881
8882 this.valueAtZero = step.marginEnd;
8883 var marginStartPos = 0;
8884
8885 // do not draw the first label
8886 var max = 1;
8887
8888 this.maxLabelSize = 0;
8889 var y = 0;
8890 while (max < Math.round(amountOfSteps)) {
8891 step.next();
8892 y = Math.round(max * stepPixels);
8893 marginStartPos = max * stepPixels;
8894 var isMajor = step.isMajor();
8895
8896 if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
8897 this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
8898 }
8899
8900 if (isMajor && this.options['showMajorLabels'] && this.master == true ||
8901 this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
8902 if (y >= 0) {
8903 this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
8904 }
8905 this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
8906 }
8907 else {
8908 this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
8909 }
8910
8911 max++;
8912 }
8913
8914 if (this.master == false) {
8915 this.conversionFactor = y / (this.valueAtZero - step.current);
8916 }
8917 else {
8918 this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange;
8919 }
8920
8921 var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
8922 // this will resize the yAxis to accomodate the labels.
8923 if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
8924 this.width = this.maxLabelSize + offset;
8925 this.options.width = this.width + "px";
8926 DOMutil.cleanupElements(this.DOMelements.lines);
8927 DOMutil.cleanupElements(this.DOMelements.labels);
8928 this.redraw();
8929 return true;
8930 }
8931 // this will resize the yAxis if it is too big for the labels.
8932 else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
8933 this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
8934 this.options.width = this.width + "px";
8935 DOMutil.cleanupElements(this.DOMelements.lines);
8936 DOMutil.cleanupElements(this.DOMelements.labels);
8937 this.redraw();
8938 return true;
8939 }
8940 else {
8941 DOMutil.cleanupElements(this.DOMelements.lines);
8942 DOMutil.cleanupElements(this.DOMelements.labels);
8943 return false;
8944 }
8945 };
8946
8947 DataAxis.prototype.convertValue = function (value) {
8948 var invertedValue = this.valueAtZero - value;
8949 var convertedValue = invertedValue * this.conversionFactor;
8950 return convertedValue;
8951 };
8952
8953 /**
8954 * Create a label for the axis at position x
8955 * @private
8956 * @param y
8957 * @param text
8958 * @param orientation
8959 * @param className
8960 * @param characterHeight
8961 */
8962 DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
8963 // reuse redundant label
8964 var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
8965 label.className = className;
8966 label.innerHTML = text;
8967 if (orientation == 'left') {
8968 label.style.left = '-' + this.options.labelOffsetX + 'px';
8969 label.style.textAlign = "right";
8970 }
8971 else {
8972 label.style.right = '-' + this.options.labelOffsetX + 'px';
8973 label.style.textAlign = "left";
8974 }
8975
8976 label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
8977
8978 text += '';
8979
8980 var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
8981 if (this.maxLabelSize < text.length * largestWidth) {
8982 this.maxLabelSize = text.length * largestWidth;
8983 }
8984 };
8985
8986 /**
8987 * Create a minor line for the axis at position y
8988 * @param y
8989 * @param orientation
8990 * @param className
8991 * @param offset
8992 * @param width
8993 */
8994 DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
8995 if (this.master == true) {
8996 var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
8997 line.className = className;
8998 line.innerHTML = '';
8999
9000 if (orientation == 'left') {
9001 line.style.left = (this.width - offset) + 'px';
9002 }
9003 else {
9004 line.style.right = (this.width - offset) + 'px';
9005 }
9006
9007 line.style.width = width + 'px';
9008 line.style.top = y + 'px';
9009 }
9010 };
9011
9012
9013
9014
9015
9016 /**
9017 * Determine the size of text on the axis (both major and minor axis).
9018 * The size is calculated only once and then cached in this.props.
9019 * @private
9020 */
9021 DataAxis.prototype._calculateCharSize = function () {
9022 // determine the char width and height on the minor axis
9023 if (!('minorCharHeight' in this.props)) {
9024 var textMinor = document.createTextNode('0');
9025 var measureCharMinor = document.createElement('DIV');
9026 measureCharMinor.className = 'yAxis minor measure';
9027 measureCharMinor.appendChild(textMinor);
9028 this.dom.frame.appendChild(measureCharMinor);
9029
9030 this.props.minorCharHeight = measureCharMinor.clientHeight;
9031 this.props.minorCharWidth = measureCharMinor.clientWidth;
9032
9033 this.dom.frame.removeChild(measureCharMinor);
9034 }
9035
9036 if (!('majorCharHeight' in this.props)) {
9037 var textMajor = document.createTextNode('0');
9038 var measureCharMajor = document.createElement('DIV');
9039 measureCharMajor.className = 'yAxis major measure';
9040 measureCharMajor.appendChild(textMajor);
9041 this.dom.frame.appendChild(measureCharMajor);
9042
9043 this.props.majorCharHeight = measureCharMajor.clientHeight;
9044 this.props.majorCharWidth = measureCharMajor.clientWidth;
9045
9046 this.dom.frame.removeChild(measureCharMajor);
9047 }
9048 };
9049
9050 /**
9051 * Snap a date to a rounded value.
9052 * The snap intervals are dependent on the current scale and step.
9053 * @param {Date} date the date to be snapped.
9054 * @return {Date} snappedDate
9055 */
9056 DataAxis.prototype.snap = function(date) {
9057 return this.step.snap(date);
9058 };
9059
9060 module.exports = DataAxis;
9061
9062
9063/***/ },
9064/* 22 */
9065/***/ function(module, exports, __webpack_require__) {
9066
9067 var util = __webpack_require__(1);
9068 var DOMutil = __webpack_require__(2);
9069
9070 /**
9071 * @constructor Group
9072 * @param {Number | String} groupId
9073 * @param {Object} data
9074 * @param {ItemSet} itemSet
9075 */
9076 function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
9077 this.id = groupId;
9078 var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
9079 this.options = util.selectiveBridgeObject(fields,options);
9080 this.usingDefaultStyle = group.className === undefined;
9081 this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
9082 this.zeroPosition = 0;
9083 this.update(group);
9084 if (this.usingDefaultStyle == true) {
9085 this.groupsUsingDefaultStyles[0] += 1;
9086 }
9087 this.itemsData = [];
9088 this.visible = group.visible === undefined ? true : group.visible;
9089 }
9090
9091 GraphGroup.prototype.setItems = function(items) {
9092 if (items != null) {
9093 this.itemsData = items;
9094 if (this.options.sort == true) {
9095 this.itemsData.sort(function (a,b) {return a.x - b.x;})
9096 }
9097 }
9098 else {
9099 this.itemsData = [];
9100 }
9101 };
9102
9103 GraphGroup.prototype.setZeroPosition = function(pos) {
9104 this.zeroPosition = pos;
9105 };
9106
9107 GraphGroup.prototype.setOptions = function(options) {
9108 if (options !== undefined) {
9109 var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
9110 util.selectiveDeepExtend(fields, this.options, options);
9111
9112 util.mergeOptions(this.options, options,'catmullRom');
9113 util.mergeOptions(this.options, options,'drawPoints');
9114 util.mergeOptions(this.options, options,'shaded');
9115
9116 if (options.catmullRom) {
9117 if (typeof options.catmullRom == 'object') {
9118 if (options.catmullRom.parametrization) {
9119 if (options.catmullRom.parametrization == 'uniform') {
9120 this.options.catmullRom.alpha = 0;
9121 }
9122 else if (options.catmullRom.parametrization == 'chordal') {
9123 this.options.catmullRom.alpha = 1.0;
9124 }
9125 else {
9126 this.options.catmullRom.parametrization = 'centripetal';
9127 this.options.catmullRom.alpha = 0.5;
9128 }
9129 }
9130 }
9131 }
9132 }
9133 };
9134
9135 GraphGroup.prototype.update = function(group) {
9136 this.group = group;
9137 this.content = group.content || 'graph';
9138 this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
9139 this.visible = group.visible === undefined ? true : group.visible;
9140 this.setOptions(group.options);
9141 };
9142
9143 GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
9144 var fillHeight = iconHeight * 0.5;
9145 var path, fillPath;
9146
9147 var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
9148 outline.setAttributeNS(null, "x", x);
9149 outline.setAttributeNS(null, "y", y - fillHeight);
9150 outline.setAttributeNS(null, "width", iconWidth);
9151 outline.setAttributeNS(null, "height", 2*fillHeight);
9152 outline.setAttributeNS(null, "class", "outline");
9153
9154 if (this.options.style == 'line') {
9155 path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
9156 path.setAttributeNS(null, "class", this.className);
9157 path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
9158 if (this.options.shaded.enabled == true) {
9159 fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
9160 if (this.options.shaded.orientation == 'top') {
9161 fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
9162 "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
9163 }
9164 else {
9165 fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
9166 "L"+x+"," + (y + fillHeight) + " " +
9167 "L"+ (x + iconWidth) + "," + (y + fillHeight) +
9168 "L"+ (x + iconWidth) + ","+y);
9169 }
9170 fillPath.setAttributeNS(null, "class", this.className + " iconFill");
9171 }
9172
9173 if (this.options.drawPoints.enabled == true) {
9174 DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
9175 }
9176 }
9177 else {
9178 var barWidth = Math.round(0.3 * iconWidth);
9179 var bar1Height = Math.round(0.4 * iconHeight);
9180 var bar2Height = Math.round(0.75 * iconHeight);
9181
9182 var offset = Math.round((iconWidth - (2 * barWidth))/3);
9183
9184 DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
9185 DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
9186 }
9187 };
9188
9189 /**
9190 *
9191 * @param iconWidth
9192 * @param iconHeight
9193 * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
9194 */
9195 GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
9196 var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
9197 this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
9198 return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
9199 }
9200
9201 module.exports = GraphGroup;
9202
9203
9204/***/ },
9205/* 23 */
9206/***/ function(module, exports, __webpack_require__) {
9207
9208 var util = __webpack_require__(1);
9209 var stack = __webpack_require__(16);
9210 var RangeItem = __webpack_require__(32);
9211
9212 /**
9213 * @constructor Group
9214 * @param {Number | String} groupId
9215 * @param {Object} data
9216 * @param {ItemSet} itemSet
9217 */
9218 function Group (groupId, data, itemSet) {
9219 this.groupId = groupId;
9220
9221 this.itemSet = itemSet;
9222
9223 this.dom = {};
9224 this.props = {
9225 label: {
9226 width: 0,
9227 height: 0
9228 }
9229 };
9230 this.className = null;
9231
9232 this.items = {}; // items filtered by groupId of this group
9233 this.visibleItems = []; // items currently visible in window
9234 this.orderedItems = { // items sorted by start and by end
9235 byStart: [],
9236 byEnd: []
9237 };
9238
9239 this._create();
9240
9241 this.setData(data);
9242 }
9243
9244 /**
9245 * Create DOM elements for the group
9246 * @private
9247 */
9248 Group.prototype._create = function() {
9249 var label = document.createElement('div');
9250 label.className = 'vlabel';
9251 this.dom.label = label;
9252
9253 var inner = document.createElement('div');
9254 inner.className = 'inner';
9255 label.appendChild(inner);
9256 this.dom.inner = inner;
9257
9258 var foreground = document.createElement('div');
9259 foreground.className = 'group';
9260 foreground['timeline-group'] = this;
9261 this.dom.foreground = foreground;
9262
9263 this.dom.background = document.createElement('div');
9264 this.dom.background.className = 'group';
9265
9266 this.dom.axis = document.createElement('div');
9267 this.dom.axis.className = 'group';
9268
9269 // create a hidden marker to detect when the Timelines container is attached
9270 // to the DOM, or the style of a parent of the Timeline is changed from
9271 // display:none is changed to visible.
9272 this.dom.marker = document.createElement('div');
9273 this.dom.marker.style.visibility = 'hidden';
9274 this.dom.marker.innerHTML = '?';
9275 this.dom.background.appendChild(this.dom.marker);
9276 };
9277
9278 /**
9279 * Set the group data for this group
9280 * @param {Object} data Group data, can contain properties content and className
9281 */
9282 Group.prototype.setData = function(data) {
9283 // update contents
9284 var content = data && data.content;
9285 if (content instanceof Element) {
9286 this.dom.inner.appendChild(content);
9287 }
9288 else if (content !== undefined && content !== null) {
9289 this.dom.inner.innerHTML = content;
9290 }
9291 else {
9292 this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
9293 }
9294
9295 // update title
9296 this.dom.label.title = data && data.title || '';
9297
9298 if (!this.dom.inner.firstChild) {
9299 util.addClassName(this.dom.inner, 'hidden');
9300 }
9301 else {
9302 util.removeClassName(this.dom.inner, 'hidden');
9303 }
9304
9305 // update className
9306 var className = data && data.className || null;
9307 if (className != this.className) {
9308 if (this.className) {
9309 util.removeClassName(this.dom.label, this.className);
9310 util.removeClassName(this.dom.foreground, this.className);
9311 util.removeClassName(this.dom.background, this.className);
9312 util.removeClassName(this.dom.axis, this.className);
9313 }
9314 util.addClassName(this.dom.label, className);
9315 util.addClassName(this.dom.foreground, className);
9316 util.addClassName(this.dom.background, className);
9317 util.addClassName(this.dom.axis, className);
9318 this.className = className;
9319 }
9320 };
9321
9322 /**
9323 * Get the width of the group label
9324 * @return {number} width
9325 */
9326 Group.prototype.getLabelWidth = function() {
9327 return this.props.label.width;
9328 };
9329
9330
9331 /**
9332 * Repaint this group
9333 * @param {{start: number, end: number}} range
9334 * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
9335 * @param {boolean} [restack=false] Force restacking of all items
9336 * @return {boolean} Returns true if the group is resized
9337 */
9338 Group.prototype.redraw = function(range, margin, restack) {
9339 var resized = false;
9340
9341 this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
9342
9343 // force recalculation of the height of the items when the marker height changed
9344 // (due to the Timeline being attached to the DOM or changed from display:none to visible)
9345 var markerHeight = this.dom.marker.clientHeight;
9346 if (markerHeight != this.lastMarkerHeight) {
9347 this.lastMarkerHeight = markerHeight;
9348
9349 util.forEach(this.items, function (item) {
9350 item.dirty = true;
9351 if (item.displayed) item.redraw();
9352 });
9353
9354 restack = true;
9355 }
9356
9357 // reposition visible items vertically
9358 if (this.itemSet.options.stack) { // TODO: ugly way to access options...
9359 stack.stack(this.visibleItems, margin, restack);
9360 }
9361 else { // no stacking
9362 stack.nostack(this.visibleItems, margin);
9363 }
9364
9365 // recalculate the height of the group
9366 var height;
9367 var visibleItems = this.visibleItems;
9368 if (visibleItems.length) {
9369 var min = visibleItems[0].top;
9370 var max = visibleItems[0].top + visibleItems[0].height;
9371 util.forEach(visibleItems, function (item) {
9372 min = Math.min(min, item.top);
9373 max = Math.max(max, (item.top + item.height));
9374 });
9375 if (min > margin.axis) {
9376 // there is an empty gap between the lowest item and the axis
9377 var offset = min - margin.axis;
9378 max -= offset;
9379 util.forEach(visibleItems, function (item) {
9380 item.top -= offset;
9381 });
9382 }
9383 height = max + margin.item.vertical / 2;
9384 }
9385 else {
9386 height = margin.axis + margin.item.vertical;
9387 }
9388 height = Math.max(height, this.props.label.height);
9389
9390 // calculate actual size and position
9391 var foreground = this.dom.foreground;
9392 this.top = foreground.offsetTop;
9393 this.left = foreground.offsetLeft;
9394 this.width = foreground.offsetWidth;
9395 resized = util.updateProperty(this, 'height', height) || resized;
9396
9397 // recalculate size of label
9398 resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
9399 resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
9400
9401 // apply new height
9402 this.dom.background.style.height = height + 'px';
9403 this.dom.foreground.style.height = height + 'px';
9404 this.dom.label.style.height = height + 'px';
9405
9406 // update vertical position of items after they are re-stacked and the height of the group is calculated
9407 for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
9408 var item = this.visibleItems[i];
9409 item.repositionY();
9410 }
9411
9412 return resized;
9413 };
9414
9415 /**
9416 * Show this group: attach to the DOM
9417 */
9418 Group.prototype.show = function() {
9419 if (!this.dom.label.parentNode) {
9420 this.itemSet.dom.labelSet.appendChild(this.dom.label);
9421 }
9422
9423 if (!this.dom.foreground.parentNode) {
9424 this.itemSet.dom.foreground.appendChild(this.dom.foreground);
9425 }
9426
9427 if (!this.dom.background.parentNode) {
9428 this.itemSet.dom.background.appendChild(this.dom.background);
9429 }
9430
9431 if (!this.dom.axis.parentNode) {
9432 this.itemSet.dom.axis.appendChild(this.dom.axis);
9433 }
9434 };
9435
9436 /**
9437 * Hide this group: remove from the DOM
9438 */
9439 Group.prototype.hide = function() {
9440 var label = this.dom.label;
9441 if (label.parentNode) {
9442 label.parentNode.removeChild(label);
9443 }
9444
9445 var foreground = this.dom.foreground;
9446 if (foreground.parentNode) {
9447 foreground.parentNode.removeChild(foreground);
9448 }
9449
9450 var background = this.dom.background;
9451 if (background.parentNode) {
9452 background.parentNode.removeChild(background);
9453 }
9454
9455 var axis = this.dom.axis;
9456 if (axis.parentNode) {
9457 axis.parentNode.removeChild(axis);
9458 }
9459 };
9460
9461 /**
9462 * Add an item to the group
9463 * @param {Item} item
9464 */
9465 Group.prototype.add = function(item) {
9466 this.items[item.id] = item;
9467 item.setParent(this);
9468
9469 if (this.visibleItems.indexOf(item) == -1) {
9470 var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
9471 this._checkIfVisible(item, this.visibleItems, range);
9472 }
9473 };
9474
9475 /**
9476 * Remove an item from the group
9477 * @param {Item} item
9478 */
9479 Group.prototype.remove = function(item) {
9480 delete this.items[item.id];
9481 item.setParent(this.itemSet);
9482
9483 // remove from visible items
9484 var index = this.visibleItems.indexOf(item);
9485 if (index != -1) this.visibleItems.splice(index, 1);
9486
9487 // TODO: also remove from ordered items?
9488 };
9489
9490 /**
9491 * Remove an item from the corresponding DataSet
9492 * @param {Item} item
9493 */
9494 Group.prototype.removeFromDataSet = function(item) {
9495 this.itemSet.removeItem(item.id);
9496 };
9497
9498 /**
9499 * Reorder the items
9500 */
9501 Group.prototype.order = function() {
9502 var array = util.toArray(this.items);
9503 this.orderedItems.byStart = array;
9504 this.orderedItems.byEnd = this._constructByEndArray(array);
9505
9506 stack.orderByStart(this.orderedItems.byStart);
9507 stack.orderByEnd(this.orderedItems.byEnd);
9508 };
9509
9510 /**
9511 * Create an array containing all items being a range (having an end date)
9512 * @param {Item[]} array
9513 * @returns {RangeItem[]}
9514 * @private
9515 */
9516 Group.prototype._constructByEndArray = function(array) {
9517 var endArray = [];
9518
9519 for (var i = 0; i < array.length; i++) {
9520 if (array[i] instanceof RangeItem) {
9521 endArray.push(array[i]);
9522 }
9523 }
9524 return endArray;
9525 };
9526
9527 /**
9528 * Update the visible items
9529 * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
9530 * @param {Item[]} visibleItems The previously visible items.
9531 * @param {{start: number, end: number}} range Visible range
9532 * @return {Item[]} visibleItems The new visible items.
9533 * @private
9534 */
9535 Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
9536 var initialPosByStart,
9537 newVisibleItems = [],
9538 i;
9539
9540 // first check if the items that were in view previously are still in view.
9541 // this handles the case for the RangeItem that is both before and after the current one.
9542 if (visibleItems.length > 0) {
9543 for (i = 0; i < visibleItems.length; i++) {
9544 this._checkIfVisible(visibleItems[i], newVisibleItems, range);
9545 }
9546 }
9547
9548 // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime)
9549 if (newVisibleItems.length == 0) {
9550 initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
9551 }
9552 else {
9553 initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
9554 }
9555
9556 // use visible search to find a visible RangeItem (only based on endTime)
9557 var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
9558
9559 // if we found a initial ID to use, trace it up and down until we meet an invisible item.
9560 if (initialPosByStart != -1) {
9561 for (i = initialPosByStart; i >= 0; i--) {
9562 if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
9563 }
9564 for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
9565 if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
9566 }
9567 }
9568
9569 // if we found a initial ID to use, trace it up and down until we meet an invisible item.
9570 if (initialPosByEnd != -1) {
9571 for (i = initialPosByEnd; i >= 0; i--) {
9572 if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
9573 }
9574 for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
9575 if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
9576 }
9577 }
9578
9579 return newVisibleItems;
9580 };
9581
9582
9583
9584 /**
9585 * this function checks if an item is invisible. If it is NOT we make it visible
9586 * and add it to the global visible items. If it is, return true.
9587 *
9588 * @param {Item} item
9589 * @param {Item[]} visibleItems
9590 * @param {{start:number, end:number}} range
9591 * @returns {boolean}
9592 * @private
9593 */
9594 Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
9595 if (item.isVisible(range)) {
9596 if (!item.displayed) item.show();
9597 item.repositionX();
9598 if (visibleItems.indexOf(item) == -1) {
9599 visibleItems.push(item);
9600 }
9601 return false;
9602 }
9603 else {
9604 if (item.displayed) item.hide();
9605 return true;
9606 }
9607 };
9608
9609 /**
9610 * this function is very similar to the _checkIfInvisible() but it does not
9611 * return booleans, hides the item if it should not be seen and always adds to
9612 * the visibleItems.
9613 * this one is for brute forcing and hiding.
9614 *
9615 * @param {Item} item
9616 * @param {Array} visibleItems
9617 * @param {{start:number, end:number}} range
9618 * @private
9619 */
9620 Group.prototype._checkIfVisible = function(item, visibleItems, range) {
9621 if (item.isVisible(range)) {
9622 if (!item.displayed) item.show();
9623 // reposition item horizontally
9624 item.repositionX();
9625 visibleItems.push(item);
9626 }
9627 else {
9628 if (item.displayed) item.hide();
9629 }
9630 };
9631
9632 module.exports = Group;
9633
9634
9635/***/ },
9636/* 24 */
9637/***/ function(module, exports, __webpack_require__) {
9638
9639 var Hammer = __webpack_require__(42);
9640 var util = __webpack_require__(1);
9641 var DataSet = __webpack_require__(3);
9642 var DataView = __webpack_require__(4);
9643 var Component = __webpack_require__(18);
9644 var Group = __webpack_require__(23);
9645 var BoxItem = __webpack_require__(30);
9646 var PointItem = __webpack_require__(31);
9647 var RangeItem = __webpack_require__(32);
9648 var BackgroundItem = __webpack_require__(29);
9649
9650
9651 var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
9652
9653 /**
9654 * An ItemSet holds a set of items and ranges which can be displayed in a
9655 * range. The width is determined by the parent of the ItemSet, and the height
9656 * is determined by the size of the items.
9657 * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
9658 * @param {Object} [options] See ItemSet.setOptions for the available options.
9659 * @constructor ItemSet
9660 * @extends Component
9661 */
9662 function ItemSet(body, options) {
9663 this.body = body;
9664
9665 this.defaultOptions = {
9666 type: null, // 'box', 'point', 'range', 'background'
9667 orientation: 'bottom', // 'top' or 'bottom'
9668 align: 'auto', // alignment of box items
9669 stack: true,
9670 groupOrder: null,
9671
9672 selectable: true,
9673 editable: {
9674 updateTime: false,
9675 updateGroup: false,
9676 add: false,
9677 remove: false
9678 },
9679
9680 onAdd: function (item, callback) {
9681 callback(item);
9682 },
9683 onUpdate: function (item, callback) {
9684 callback(item);
9685 },
9686 onMove: function (item, callback) {
9687 callback(item);
9688 },
9689 onRemove: function (item, callback) {
9690 callback(item);
9691 },
9692 onMoving: function (item, callback) {
9693 callback(item);
9694 },
9695
9696 margin: {
9697 item: {
9698 horizontal: 10,
9699 vertical: 10
9700 },
9701 axis: 20
9702 },
9703 padding: 5
9704 };
9705
9706 // options is shared by this ItemSet and all its items
9707 this.options = util.extend({}, this.defaultOptions);
9708
9709 // options for getting items from the DataSet with the correct type
9710 this.itemOptions = {
9711 type: {start: 'Date', end: 'Date'}
9712 };
9713
9714 this.conversion = {
9715 toScreen: body.util.toScreen,
9716 toTime: body.util.toTime
9717 };
9718 this.dom = {};
9719 this.props = {};
9720 this.hammer = null;
9721
9722 var me = this;
9723 this.itemsData = null; // DataSet
9724 this.groupsData = null; // DataSet
9725
9726 // listeners for the DataSet of the items
9727 this.itemListeners = {
9728 'add': function (event, params, senderId) {
9729 me._onAdd(params.items);
9730 },
9731 'update': function (event, params, senderId) {
9732 me._onUpdate(params.items);
9733 },
9734 'remove': function (event, params, senderId) {
9735 me._onRemove(params.items);
9736 }
9737 };
9738
9739 // listeners for the DataSet of the groups
9740 this.groupListeners = {
9741 'add': function (event, params, senderId) {
9742 me._onAddGroups(params.items);
9743 },
9744 'update': function (event, params, senderId) {
9745 me._onUpdateGroups(params.items);
9746 },
9747 'remove': function (event, params, senderId) {
9748 me._onRemoveGroups(params.items);
9749 }
9750 };
9751
9752 this.items = {}; // object with an Item for every data item
9753 this.groups = {}; // Group object for every group
9754 this.groupIds = [];
9755
9756 this.selection = []; // list with the ids of all selected nodes
9757 this.stackDirty = true; // if true, all items will be restacked on next redraw
9758
9759 this.touchParams = {}; // stores properties while dragging
9760 // create the HTML DOM
9761
9762 this._create();
9763
9764 this.setOptions(options);
9765 }
9766
9767 ItemSet.prototype = new Component();
9768
9769 // available item types will be registered here
9770 ItemSet.types = {
9771 background: BackgroundItem,
9772 box: BoxItem,
9773 range: RangeItem,
9774 point: PointItem
9775 };
9776
9777 /**
9778 * Create the HTML DOM for the ItemSet
9779 */
9780 ItemSet.prototype._create = function(){
9781 var frame = document.createElement('div');
9782 frame.className = 'itemset';
9783 frame['timeline-itemset'] = this;
9784 this.dom.frame = frame;
9785
9786 // create background panel
9787 var background = document.createElement('div');
9788 background.className = 'background';
9789 frame.appendChild(background);
9790 this.dom.background = background;
9791
9792 // create foreground panel
9793 var foreground = document.createElement('div');
9794 foreground.className = 'foreground';
9795 frame.appendChild(foreground);
9796 this.dom.foreground = foreground;
9797
9798 // create axis panel
9799 var axis = document.createElement('div');
9800 axis.className = 'axis';
9801 this.dom.axis = axis;
9802
9803 // create labelset
9804 var labelSet = document.createElement('div');
9805 labelSet.className = 'labelset';
9806 this.dom.labelSet = labelSet;
9807
9808 // create ungrouped Group
9809 this._updateUngrouped();
9810
9811 // attach event listeners
9812 // Note: we bind to the centerContainer for the case where the height
9813 // of the center container is larger than of the ItemSet, so we
9814 // can click in the empty area to create a new item or deselect an item.
9815 this.hammer = Hammer(this.body.dom.centerContainer, {
9816 prevent_default: true
9817 });
9818
9819 // drag items when selected
9820 this.hammer.on('touch', this._onTouch.bind(this));
9821 this.hammer.on('dragstart', this._onDragStart.bind(this));
9822 this.hammer.on('drag', this._onDrag.bind(this));
9823 this.hammer.on('dragend', this._onDragEnd.bind(this));
9824
9825 // single select (or unselect) when tapping an item
9826 this.hammer.on('tap', this._onSelectItem.bind(this));
9827
9828 // multi select when holding mouse/touch, or on ctrl+click
9829 this.hammer.on('hold', this._onMultiSelectItem.bind(this));
9830
9831 // add item on doubletap
9832 this.hammer.on('doubletap', this._onAddItem.bind(this));
9833
9834 // attach to the DOM
9835 this.show();
9836 };
9837
9838 /**
9839 * Set options for the ItemSet. Existing options will be extended/overwritten.
9840 * @param {Object} [options] The following options are available:
9841 * {String} type
9842 * Default type for the items. Choose from 'box'
9843 * (default), 'point', 'range', or 'background'.
9844 * The default style can be overwritten by
9845 * individual items.
9846 * {String} align
9847 * Alignment for the items, only applicable for
9848 * BoxItem. Choose 'center' (default), 'left', or
9849 * 'right'.
9850 * {String} orientation
9851 * Orientation of the item set. Choose 'top' or
9852 * 'bottom' (default).
9853 * {Function} groupOrder
9854 * A sorting function for ordering groups
9855 * {Boolean} stack
9856 * If true (deafult), items will be stacked on
9857 * top of each other.
9858 * {Number} margin.axis
9859 * Margin between the axis and the items in pixels.
9860 * Default is 20.
9861 * {Number} margin.item.horizontal
9862 * Horizontal margin between items in pixels.
9863 * Default is 10.
9864 * {Number} margin.item.vertical
9865 * Vertical Margin between items in pixels.
9866 * Default is 10.
9867 * {Number} margin.item
9868 * Margin between items in pixels in both horizontal
9869 * and vertical direction. Default is 10.
9870 * {Number} margin
9871 * Set margin for both axis and items in pixels.
9872 * {Number} padding
9873 * Padding of the contents of an item in pixels.
9874 * Must correspond with the items css. Default is 5.
9875 * {Boolean} selectable
9876 * If true (default), items can be selected.
9877 * {Boolean} editable
9878 * Set all editable options to true or false
9879 * {Boolean} editable.updateTime
9880 * Allow dragging an item to an other moment in time
9881 * {Boolean} editable.updateGroup
9882 * Allow dragging an item to an other group
9883 * {Boolean} editable.add
9884 * Allow creating new items on double tap
9885 * {Boolean} editable.remove
9886 * Allow removing items by clicking the delete button
9887 * top right of a selected item.
9888 * {Function(item: Item, callback: Function)} onAdd
9889 * Callback function triggered when an item is about to be added:
9890 * when the user double taps an empty space in the Timeline.
9891 * {Function(item: Item, callback: Function)} onUpdate
9892 * Callback function fired when an item is about to be updated.
9893 * This function typically has to show a dialog where the user
9894 * change the item. If not implemented, nothing happens.
9895 * {Function(item: Item, callback: Function)} onMove
9896 * Fired when an item has been moved. If not implemented,
9897 * the move action will be accepted.
9898 * {Function(item: Item, callback: Function)} onRemove
9899 * Fired when an item is about to be deleted.
9900 * If not implemented, the item will be always removed.
9901 */
9902 ItemSet.prototype.setOptions = function(options) {
9903 if (options) {
9904 // copy all options that we know
9905 var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template'];
9906 util.selectiveExtend(fields, this.options, options);
9907
9908 if ('margin' in options) {
9909 if (typeof options.margin === 'number') {
9910 this.options.margin.axis = options.margin;
9911 this.options.margin.item.horizontal = options.margin;
9912 this.options.margin.item.vertical = options.margin;
9913 }
9914 else if (typeof options.margin === 'object') {
9915 util.selectiveExtend(['axis'], this.options.margin, options.margin);
9916 if ('item' in options.margin) {
9917 if (typeof options.margin.item === 'number') {
9918 this.options.margin.item.horizontal = options.margin.item;
9919 this.options.margin.item.vertical = options.margin.item;
9920 }
9921 else if (typeof options.margin.item === 'object') {
9922 util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item);
9923 }
9924 }
9925 }
9926 }
9927
9928 if ('editable' in options) {
9929 if (typeof options.editable === 'boolean') {
9930 this.options.editable.updateTime = options.editable;
9931 this.options.editable.updateGroup = options.editable;
9932 this.options.editable.add = options.editable;
9933 this.options.editable.remove = options.editable;
9934 }
9935 else if (typeof options.editable === 'object') {
9936 util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
9937 }
9938 }
9939
9940 // callback functions
9941 var addCallback = (function (name) {
9942 var fn = options[name];
9943 if (fn) {
9944 if (!(fn instanceof Function)) {
9945 throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
9946 }
9947 this.options[name] = fn;
9948 }
9949 }).bind(this);
9950 ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback);
9951
9952 // force the itemSet to refresh: options like orientation and margins may be changed
9953 this.markDirty();
9954 }
9955 };
9956
9957 /**
9958 * Mark the ItemSet dirty so it will refresh everything with next redraw
9959 */
9960 ItemSet.prototype.markDirty = function() {
9961 this.groupIds = [];
9962 this.stackDirty = true;
9963 };
9964
9965 /**
9966 * Destroy the ItemSet
9967 */
9968 ItemSet.prototype.destroy = function() {
9969 this.hide();
9970 this.setItems(null);
9971 this.setGroups(null);
9972
9973 this.hammer = null;
9974
9975 this.body = null;
9976 this.conversion = null;
9977 };
9978
9979 /**
9980 * Hide the component from the DOM
9981 */
9982 ItemSet.prototype.hide = function() {
9983 // remove the frame containing the items
9984 if (this.dom.frame.parentNode) {
9985 this.dom.frame.parentNode.removeChild(this.dom.frame);
9986 }
9987
9988 // remove the axis with dots
9989 if (this.dom.axis.parentNode) {
9990 this.dom.axis.parentNode.removeChild(this.dom.axis);
9991 }
9992
9993 // remove the labelset containing all group labels
9994 if (this.dom.labelSet.parentNode) {
9995 this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
9996 }
9997 };
9998
9999 /**
10000 * Show the component in the DOM (when not already visible).
10001 * @return {Boolean} changed
10002 */
10003 ItemSet.prototype.show = function() {
10004 // show frame containing the items
10005 if (!this.dom.frame.parentNode) {
10006 this.body.dom.center.appendChild(this.dom.frame);
10007 }
10008
10009 // show axis with dots
10010 if (!this.dom.axis.parentNode) {
10011 this.body.dom.backgroundVertical.appendChild(this.dom.axis);
10012 }
10013
10014 // show labelset containing labels
10015 if (!this.dom.labelSet.parentNode) {
10016 this.body.dom.left.appendChild(this.dom.labelSet);
10017 }
10018 };
10019
10020 /**
10021 * Set selected items by their id. Replaces the current selection
10022 * Unknown id's are silently ignored.
10023 * @param {string[] | string} [ids] An array with zero or more id's of the items to be
10024 * selected, or a single item id. If ids is undefined
10025 * or an empty array, all items will be unselected.
10026 */
10027 ItemSet.prototype.setSelection = function(ids) {
10028 var i, ii, id, item;
10029
10030 if (ids == undefined) ids = [];
10031 if (!Array.isArray(ids)) ids = [ids];
10032
10033 // unselect currently selected items
10034 for (i = 0, ii = this.selection.length; i < ii; i++) {
10035 id = this.selection[i];
10036 item = this.items[id];
10037 if (item) item.unselect();
10038 }
10039
10040 // select items
10041 this.selection = [];
10042 for (i = 0, ii = ids.length; i < ii; i++) {
10043 id = ids[i];
10044 item = this.items[id];
10045 if (item) {
10046 this.selection.push(id);
10047 item.select();
10048 }
10049 }
10050 };
10051
10052 /**
10053 * Get the selected items by their id
10054 * @return {Array} ids The ids of the selected items
10055 */
10056 ItemSet.prototype.getSelection = function() {
10057 return this.selection.concat([]);
10058 };
10059
10060 /**
10061 * Get the id's of the currently visible items.
10062 * @returns {Array} The ids of the visible items
10063 */
10064 ItemSet.prototype.getVisibleItems = function() {
10065 var range = this.body.range.getRange();
10066 var left = this.body.util.toScreen(range.start);
10067 var right = this.body.util.toScreen(range.end);
10068
10069 var ids = [];
10070 for (var groupId in this.groups) {
10071 if (this.groups.hasOwnProperty(groupId)) {
10072 var group = this.groups[groupId];
10073 var rawVisibleItems = group.visibleItems;
10074
10075 // filter the "raw" set with visibleItems into a set which is really
10076 // visible by pixels
10077 for (var i = 0; i < rawVisibleItems.length; i++) {
10078 var item = rawVisibleItems[i];
10079 // TODO: also check whether visible vertically
10080 if ((item.left < right) && (item.left + item.width > left)) {
10081 ids.push(item.id);
10082 }
10083 }
10084 }
10085 }
10086
10087 return ids;
10088 };
10089
10090 /**
10091 * Deselect a selected item
10092 * @param {String | Number} id
10093 * @private
10094 */
10095 ItemSet.prototype._deselect = function(id) {
10096 var selection = this.selection;
10097 for (var i = 0, ii = selection.length; i < ii; i++) {
10098 if (selection[i] == id) { // non-strict comparison!
10099 selection.splice(i, 1);
10100 break;
10101 }
10102 }
10103 };
10104
10105 /**
10106 * Repaint the component
10107 * @return {boolean} Returns true if the component is resized
10108 */
10109 ItemSet.prototype.redraw = function() {
10110 var margin = this.options.margin,
10111 range = this.body.range,
10112 asSize = util.option.asSize,
10113 options = this.options,
10114 orientation = options.orientation,
10115 resized = false,
10116 frame = this.dom.frame,
10117 editable = options.editable.updateTime || options.editable.updateGroup;
10118
10119 // recalculate absolute position (before redrawing groups)
10120 this.props.top = this.body.domProps.top.height + this.body.domProps.border.top;
10121 this.props.left = this.body.domProps.left.width + this.body.domProps.border.left;
10122
10123 // update class name
10124 frame.className = 'itemset' + (editable ? ' editable' : '');
10125
10126 // reorder the groups (if needed)
10127 resized = this._orderGroups() || resized;
10128
10129 // check whether zoomed (in that case we need to re-stack everything)
10130 // TODO: would be nicer to get this as a trigger from Range
10131 var visibleInterval = range.end - range.start;
10132 var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
10133 if (zoomed) this.stackDirty = true;
10134 this.lastVisibleInterval = visibleInterval;
10135 this.props.lastWidth = this.props.width;
10136
10137 // redraw all groups
10138 var restack = this.stackDirty,
10139 firstGroup = this._firstGroup(),
10140 firstMargin = {
10141 item: margin.item,
10142 axis: margin.axis
10143 },
10144 nonFirstMargin = {
10145 item: margin.item,
10146 axis: margin.item.vertical / 2
10147 },
10148 height = 0,
10149 minHeight = margin.axis + margin.item.vertical;
10150 util.forEach(this.groups, function (group) {
10151 var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
10152 var groupResized = group.redraw(range, groupMargin, restack);
10153 resized = groupResized || resized;
10154 height += group.height;
10155 });
10156 height = Math.max(height, minHeight);
10157 this.stackDirty = false;
10158
10159 // update frame height
10160 frame.style.height = asSize(height);
10161
10162 // calculate actual size
10163 this.props.width = frame.offsetWidth;
10164 this.props.height = height;
10165
10166 // reposition axis
10167
10168 // reposition axis
10169 this.dom.axis.style.top = asSize((orientation == 'top') ?
10170 (this.body.domProps.top.height + this.body.domProps.border.top) :
10171 (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
10172 this.dom.axis.style.left = '0';
10173
10174 // check if this component is resized
10175 resized = this._isResized() || resized;
10176
10177 return resized;
10178 };
10179
10180 /**
10181 * Get the first group, aligned with the axis
10182 * @return {Group | null} firstGroup
10183 * @private
10184 */
10185 ItemSet.prototype._firstGroup = function() {
10186 var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
10187 var firstGroupId = this.groupIds[firstGroupIndex];
10188 var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
10189
10190 return firstGroup || null;
10191 };
10192
10193 /**
10194 * Create or delete the group holding all ungrouped items. This group is used when
10195 * there are no groups specified.
10196 * @protected
10197 */
10198 ItemSet.prototype._updateUngrouped = function() {
10199 var ungrouped = this.groups[UNGROUPED];
10200
10201 if (this.groupsData) {
10202 // remove the group holding all ungrouped items
10203 if (ungrouped) {
10204 ungrouped.hide();
10205 delete this.groups[UNGROUPED];
10206 }
10207 }
10208 else {
10209 // create a group holding all (unfiltered) items
10210 if (!ungrouped) {
10211 var id = null;
10212 var data = null;
10213 ungrouped = new Group(id, data, this);
10214 this.groups[UNGROUPED] = ungrouped;
10215
10216 for (var itemId in this.items) {
10217 if (this.items.hasOwnProperty(itemId)) {
10218 ungrouped.add(this.items[itemId]);
10219 }
10220 }
10221
10222 ungrouped.show();
10223 }
10224 }
10225 };
10226
10227 /**
10228 * Get the element for the labelset
10229 * @return {HTMLElement} labelSet
10230 */
10231 ItemSet.prototype.getLabelSet = function() {
10232 return this.dom.labelSet;
10233 };
10234
10235 /**
10236 * Set items
10237 * @param {vis.DataSet | null} items
10238 */
10239 ItemSet.prototype.setItems = function(items) {
10240 var me = this,
10241 ids,
10242 oldItemsData = this.itemsData;
10243
10244 // replace the dataset
10245 if (!items) {
10246 this.itemsData = null;
10247 }
10248 else if (items instanceof DataSet || items instanceof DataView) {
10249 this.itemsData = items;
10250 }
10251 else {
10252 throw new TypeError('Data must be an instance of DataSet or DataView');
10253 }
10254
10255 if (oldItemsData) {
10256 // unsubscribe from old dataset
10257 util.forEach(this.itemListeners, function (callback, event) {
10258 oldItemsData.off(event, callback);
10259 });
10260
10261 // remove all drawn items
10262 ids = oldItemsData.getIds();
10263 this._onRemove(ids);
10264 }
10265
10266 if (this.itemsData) {
10267 // subscribe to new dataset
10268 var id = this.id;
10269 util.forEach(this.itemListeners, function (callback, event) {
10270 me.itemsData.on(event, callback, id);
10271 });
10272
10273 // add all new items
10274 ids = this.itemsData.getIds();
10275 this._onAdd(ids);
10276
10277 // update the group holding all ungrouped items
10278 this._updateUngrouped();
10279 }
10280 };
10281
10282 /**
10283 * Get the current items
10284 * @returns {vis.DataSet | null}
10285 */
10286 ItemSet.prototype.getItems = function() {
10287 return this.itemsData;
10288 };
10289
10290 /**
10291 * Set groups
10292 * @param {vis.DataSet} groups
10293 */
10294 ItemSet.prototype.setGroups = function(groups) {
10295 var me = this,
10296 ids;
10297
10298 // unsubscribe from current dataset
10299 if (this.groupsData) {
10300 util.forEach(this.groupListeners, function (callback, event) {
10301 me.groupsData.unsubscribe(event, callback);
10302 });
10303
10304 // remove all drawn groups
10305 ids = this.groupsData.getIds();
10306 this.groupsData = null;
10307 this._onRemoveGroups(ids); // note: this will cause a redraw
10308 }
10309
10310 // replace the dataset
10311 if (!groups) {
10312 this.groupsData = null;
10313 }
10314 else if (groups instanceof DataSet || groups instanceof DataView) {
10315 this.groupsData = groups;
10316 }
10317 else {
10318 throw new TypeError('Data must be an instance of DataSet or DataView');
10319 }
10320
10321 if (this.groupsData) {
10322 // subscribe to new dataset
10323 var id = this.id;
10324 util.forEach(this.groupListeners, function (callback, event) {
10325 me.groupsData.on(event, callback, id);
10326 });
10327
10328 // draw all ms
10329 ids = this.groupsData.getIds();
10330 this._onAddGroups(ids);
10331 }
10332
10333 // update the group holding all ungrouped items
10334 this._updateUngrouped();
10335
10336 // update the order of all items in each group
10337 this._order();
10338
10339 this.body.emitter.emit('change');
10340 };
10341
10342 /**
10343 * Get the current groups
10344 * @returns {vis.DataSet | null} groups
10345 */
10346 ItemSet.prototype.getGroups = function() {
10347 return this.groupsData;
10348 };
10349
10350 /**
10351 * Remove an item by its id
10352 * @param {String | Number} id
10353 */
10354 ItemSet.prototype.removeItem = function(id) {
10355 var item = this.itemsData.get(id),
10356 dataset = this.itemsData.getDataSet();
10357
10358 if (item) {
10359 // confirm deletion
10360 this.options.onRemove(item, function (item) {
10361 if (item) {
10362 // remove by id here, it is possible that an item has no id defined
10363 // itself, so better not delete by the item itself
10364 dataset.remove(id);
10365 }
10366 });
10367 }
10368 };
10369
10370 /**
10371 * Handle updated items
10372 * @param {Number[]} ids
10373 * @protected
10374 */
10375 ItemSet.prototype._onUpdate = function(ids) {
10376 var me = this;
10377
10378 ids.forEach(function (id) {
10379 var itemData = me.itemsData.get(id, me.itemOptions),
10380 item = me.items[id],
10381 type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
10382
10383 var constructor = ItemSet.types[type];
10384
10385 if (item) {
10386 // update item
10387 if (!constructor || !(item instanceof constructor)) {
10388 // item type has changed, delete the item and recreate it
10389 me._removeItem(item);
10390 item = null;
10391 }
10392 else {
10393 me._updateItem(item, itemData);
10394 }
10395 }
10396
10397 if (!item) {
10398 // create item
10399 if (constructor) {
10400 item = new constructor(itemData, me.conversion, me.options);
10401 item.id = id; // TODO: not so nice setting id afterwards
10402 me._addItem(item);
10403 }
10404 else if (type == 'rangeoverflow') {
10405 // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
10406 throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
10407 '.vis.timeline .item.range .content {overflow: visible;}');
10408 }
10409 else {
10410 throw new TypeError('Unknown item type "' + type + '"');
10411 }
10412 }
10413 });
10414
10415 this._order();
10416 this.stackDirty = true; // force re-stacking of all items next redraw
10417 this.body.emitter.emit('change');
10418 };
10419
10420 /**
10421 * Handle added items
10422 * @param {Number[]} ids
10423 * @protected
10424 */
10425 ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
10426
10427 /**
10428 * Handle removed items
10429 * @param {Number[]} ids
10430 * @protected
10431 */
10432 ItemSet.prototype._onRemove = function(ids) {
10433 var count = 0;
10434 var me = this;
10435 ids.forEach(function (id) {
10436 var item = me.items[id];
10437 if (item) {
10438 count++;
10439 me._removeItem(item);
10440 }
10441 });
10442
10443 if (count) {
10444 // update order
10445 this._order();
10446 this.stackDirty = true; // force re-stacking of all items next redraw
10447 this.body.emitter.emit('change');
10448 }
10449 };
10450
10451 /**
10452 * Update the order of item in all groups
10453 * @private
10454 */
10455 ItemSet.prototype._order = function() {
10456 // reorder the items in all groups
10457 // TODO: optimization: only reorder groups affected by the changed items
10458 util.forEach(this.groups, function (group) {
10459 group.order();
10460 });
10461 };
10462
10463 /**
10464 * Handle updated groups
10465 * @param {Number[]} ids
10466 * @private
10467 */
10468 ItemSet.prototype._onUpdateGroups = function(ids) {
10469 this._onAddGroups(ids);
10470 };
10471
10472 /**
10473 * Handle changed groups
10474 * @param {Number[]} ids
10475 * @private
10476 */
10477 ItemSet.prototype._onAddGroups = function(ids) {
10478 var me = this;
10479
10480 ids.forEach(function (id) {
10481 var groupData = me.groupsData.get(id);
10482 var group = me.groups[id];
10483
10484 if (!group) {
10485 // check for reserved ids
10486 if (id == UNGROUPED) {
10487 throw new Error('Illegal group id. ' + id + ' is a reserved id.');
10488 }
10489
10490 var groupOptions = Object.create(me.options);
10491 util.extend(groupOptions, {
10492 height: null
10493 });
10494
10495 group = new Group(id, groupData, me);
10496 me.groups[id] = group;
10497
10498 // add items with this groupId to the new group
10499 for (var itemId in me.items) {
10500 if (me.items.hasOwnProperty(itemId)) {
10501 var item = me.items[itemId];
10502 if (item.data.group == id) {
10503 group.add(item);
10504 }
10505 }
10506 }
10507
10508 group.order();
10509 group.show();
10510 }
10511 else {
10512 // update group
10513 group.setData(groupData);
10514 }
10515 });
10516
10517 this.body.emitter.emit('change');
10518 };
10519
10520 /**
10521 * Handle removed groups
10522 * @param {Number[]} ids
10523 * @private
10524 */
10525 ItemSet.prototype._onRemoveGroups = function(ids) {
10526 var groups = this.groups;
10527 ids.forEach(function (id) {
10528 var group = groups[id];
10529
10530 if (group) {
10531 group.hide();
10532 delete groups[id];
10533 }
10534 });
10535
10536 this.markDirty();
10537
10538 this.body.emitter.emit('change');
10539 };
10540
10541 /**
10542 * Reorder the groups if needed
10543 * @return {boolean} changed
10544 * @private
10545 */
10546 ItemSet.prototype._orderGroups = function () {
10547 if (this.groupsData) {
10548 // reorder the groups
10549 var groupIds = this.groupsData.getIds({
10550 order: this.options.groupOrder
10551 });
10552
10553 var changed = !util.equalArray(groupIds, this.groupIds);
10554 if (changed) {
10555 // hide all groups, removes them from the DOM
10556 var groups = this.groups;
10557 groupIds.forEach(function (groupId) {
10558 groups[groupId].hide();
10559 });
10560
10561 // show the groups again, attach them to the DOM in correct order
10562 groupIds.forEach(function (groupId) {
10563 groups[groupId].show();
10564 });
10565
10566 this.groupIds = groupIds;
10567 }
10568
10569 return changed;
10570 }
10571 else {
10572 return false;
10573 }
10574 };
10575
10576 /**
10577 * Add a new item
10578 * @param {Item} item
10579 * @private
10580 */
10581 ItemSet.prototype._addItem = function(item) {
10582 this.items[item.id] = item;
10583
10584 // add to group
10585 var groupId = this.groupsData ? item.data.group : UNGROUPED;
10586 var group = this.groups[groupId];
10587 if (group) group.add(item);
10588 };
10589
10590 /**
10591 * Update an existing item
10592 * @param {Item} item
10593 * @param {Object} itemData
10594 * @private
10595 */
10596 ItemSet.prototype._updateItem = function(item, itemData) {
10597 var oldGroupId = item.data.group;
10598
10599 // update the items data (will redraw the item when displayed)
10600 item.setData(itemData);
10601
10602 // update group
10603 if (oldGroupId != item.data.group) {
10604 var oldGroup = this.groups[oldGroupId];
10605 if (oldGroup) oldGroup.remove(item);
10606
10607 var groupId = this.groupsData ? item.data.group : UNGROUPED;
10608 var group = this.groups[groupId];
10609 if (group) group.add(item);
10610 }
10611 };
10612
10613 /**
10614 * Delete an item from the ItemSet: remove it from the DOM, from the map
10615 * with items, and from the map with visible items, and from the selection
10616 * @param {Item} item
10617 * @private
10618 */
10619 ItemSet.prototype._removeItem = function(item) {
10620 // remove from DOM
10621 item.hide();
10622
10623 // remove from items
10624 delete this.items[item.id];
10625
10626 // remove from selection
10627 var index = this.selection.indexOf(item.id);
10628 if (index != -1) this.selection.splice(index, 1);
10629
10630 // remove from group
10631 var groupId = this.groupsData ? item.data.group : UNGROUPED;
10632 var group = this.groups[groupId];
10633 if (group) group.remove(item);
10634 };
10635
10636 /**
10637 * Create an array containing all items being a range (having an end date)
10638 * @param array
10639 * @returns {Array}
10640 * @private
10641 */
10642 ItemSet.prototype._constructByEndArray = function(array) {
10643 var endArray = [];
10644
10645 for (var i = 0; i < array.length; i++) {
10646 if (array[i] instanceof RangeItem) {
10647 endArray.push(array[i]);
10648 }
10649 }
10650 return endArray;
10651 };
10652
10653 /**
10654 * Register the clicked item on touch, before dragStart is initiated.
10655 *
10656 * dragStart is initiated from a mousemove event, which can have left the item
10657 * already resulting in an item == null
10658 *
10659 * @param {Event} event
10660 * @private
10661 */
10662 ItemSet.prototype._onTouch = function (event) {
10663 // store the touched item, used in _onDragStart
10664 this.touchParams.item = ItemSet.itemFromTarget(event);
10665 };
10666
10667 /**
10668 * Start dragging the selected events
10669 * @param {Event} event
10670 * @private
10671 */
10672 ItemSet.prototype._onDragStart = function (event) {
10673 if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
10674 return;
10675 }
10676
10677 var item = this.touchParams.item || null,
10678 me = this,
10679 props;
10680
10681 if (item && item.selected) {
10682 var dragLeftItem = event.target.dragLeftItem;
10683 var dragRightItem = event.target.dragRightItem;
10684
10685 if (dragLeftItem) {
10686 props = {
10687 item: dragLeftItem
10688 };
10689
10690 if (me.options.editable.updateTime) {
10691 props.start = item.data.start.valueOf();
10692 }
10693 if (me.options.editable.updateGroup) {
10694 if ('group' in item.data) props.group = item.data.group;
10695 }
10696
10697 this.touchParams.itemProps = [props];
10698 }
10699 else if (dragRightItem) {
10700 props = {
10701 item: dragRightItem
10702 };
10703
10704 if (me.options.editable.updateTime) {
10705 props.end = item.data.end.valueOf();
10706 }
10707 if (me.options.editable.updateGroup) {
10708 if ('group' in item.data) props.group = item.data.group;
10709 }
10710
10711 this.touchParams.itemProps = [props];
10712 }
10713 else {
10714 this.touchParams.itemProps = this.getSelection().map(function (id) {
10715 var item = me.items[id];
10716 var props = {
10717 item: item
10718 };
10719
10720 if (me.options.editable.updateTime) {
10721 if ('start' in item.data) props.start = item.data.start.valueOf();
10722 if ('end' in item.data) props.end = item.data.end.valueOf();
10723 }
10724 if (me.options.editable.updateGroup) {
10725 if ('group' in item.data) props.group = item.data.group;
10726 }
10727
10728 return props;
10729 });
10730 }
10731
10732 event.stopPropagation();
10733 }
10734 };
10735
10736 /**
10737 * Drag selected items
10738 * @param {Event} event
10739 * @private
10740 */
10741 ItemSet.prototype._onDrag = function (event) {
10742 if (this.touchParams.itemProps) {
10743 var me = this;
10744 var range = this.body.range;
10745 var snap = this.body.util.snap || null;
10746 var deltaX = event.gesture.deltaX;
10747 var scale = (this.props.width / (range.end - range.start));
10748 var offset = deltaX / scale;
10749
10750 // move
10751 this.touchParams.itemProps.forEach(function (props) {
10752 var newProps = {};
10753
10754 if ('start' in props) {
10755 var start = new Date(props.start + offset);
10756 newProps.start = snap ? snap(start) : start;
10757 }
10758
10759 if ('end' in props) {
10760 var end = new Date(props.end + offset);
10761 newProps.end = snap ? snap(end) : end;
10762 }
10763
10764 if ('group' in props) {
10765 // drag from one group to another
10766 var group = ItemSet.groupFromTarget(event);
10767 newProps.group = group && group.groupId;
10768 }
10769
10770 // confirm moving the item
10771 var itemData = util.extend({}, props.item.data, newProps);
10772 me.options.onMoving(itemData, function (itemData) {
10773 if (itemData) {
10774 me._updateItemProps(props.item, itemData);
10775 }
10776 });
10777 });
10778
10779 this.stackDirty = true; // force re-stacking of all items next redraw
10780 this.body.emitter.emit('change');
10781
10782 event.stopPropagation();
10783 }
10784 };
10785
10786 /**
10787 * Update an items properties
10788 * @param {Item} item
10789 * @param {Object} props Can contain properties start, end, and group.
10790 * @private
10791 */
10792 ItemSet.prototype._updateItemProps = function(item, props) {
10793 // TODO: copy all properties from props to item? (also new ones)
10794 if ('start' in props) item.data.start = props.start;
10795 if ('end' in props) item.data.end = props.end;
10796 if ('group' in props && item.data.group != props.group) {
10797 this._moveToGroup(item, props.group)
10798 }
10799 };
10800
10801 /**
10802 * Move an item to another group
10803 * @param {Item} item
10804 * @param {String | Number} groupId
10805 * @private
10806 */
10807 ItemSet.prototype._moveToGroup = function(item, groupId) {
10808 var group = this.groups[groupId];
10809 if (group && group.groupId != item.data.group) {
10810 var oldGroup = item.parent;
10811 oldGroup.remove(item);
10812 oldGroup.order();
10813 group.add(item);
10814 group.order();
10815
10816 item.data.group = group.groupId;
10817 }
10818 };
10819
10820 /**
10821 * End of dragging selected items
10822 * @param {Event} event
10823 * @private
10824 */
10825 ItemSet.prototype._onDragEnd = function (event) {
10826 if (this.touchParams.itemProps) {
10827 // prepare a change set for the changed items
10828 var changes = [],
10829 me = this,
10830 dataset = this.itemsData.getDataSet();
10831
10832 var itemProps = this.touchParams.itemProps ;
10833 this.touchParams.itemProps = null;
10834 itemProps.forEach(function (props) {
10835 var id = props.item.id,
10836 itemData = me.itemsData.get(id, me.itemOptions);
10837
10838 var changed = false;
10839 if ('start' in props.item.data) {
10840 changed = (props.start != props.item.data.start.valueOf());
10841 itemData.start = util.convert(props.item.data.start,
10842 dataset._options.type && dataset._options.type.start || 'Date');
10843 }
10844 if ('end' in props.item.data) {
10845 changed = changed || (props.end != props.item.data.end.valueOf());
10846 itemData.end = util.convert(props.item.data.end,
10847 dataset._options.type && dataset._options.type.end || 'Date');
10848 }
10849 if ('group' in props.item.data) {
10850 changed = changed || (props.group != props.item.data.group);
10851 itemData.group = props.item.data.group;
10852 }
10853
10854 // only apply changes when start or end is actually changed
10855 if (changed) {
10856 me.options.onMove(itemData, function (itemData) {
10857 if (itemData) {
10858 // apply changes
10859 itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
10860 changes.push(itemData);
10861 }
10862 else {
10863 // restore original values
10864 me._updateItemProps(props.item, props);
10865
10866 me.stackDirty = true; // force re-stacking of all items next redraw
10867 me.body.emitter.emit('change');
10868 }
10869 });
10870 }
10871 });
10872
10873 // apply the changes to the data (if there are changes)
10874 if (changes.length) {
10875 dataset.update(changes);
10876 }
10877
10878 event.stopPropagation();
10879 }
10880 };
10881
10882 /**
10883 * Handle selecting/deselecting an item when tapping it
10884 * @param {Event} event
10885 * @private
10886 */
10887 ItemSet.prototype._onSelectItem = function (event) {
10888 if (!this.options.selectable) return;
10889
10890 var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
10891 var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
10892 if (ctrlKey || shiftKey) {
10893 this._onMultiSelectItem(event);
10894 return;
10895 }
10896
10897 var oldSelection = this.getSelection();
10898
10899 var item = ItemSet.itemFromTarget(event);
10900 var selection = item ? [item.id] : [];
10901 this.setSelection(selection);
10902
10903 var newSelection = this.getSelection();
10904
10905 // emit a select event,
10906 // except when old selection is empty and new selection is still empty
10907 if (newSelection.length > 0 || oldSelection.length > 0) {
10908 this.body.emitter.emit('select', {
10909 items: this.getSelection()
10910 });
10911 }
10912
10913 event.stopPropagation();
10914 };
10915
10916 /**
10917 * Handle creation and updates of an item on double tap
10918 * @param event
10919 * @private
10920 */
10921 ItemSet.prototype._onAddItem = function (event) {
10922 if (!this.options.selectable) return;
10923 if (!this.options.editable.add) return;
10924
10925 var me = this,
10926 snap = this.body.util.snap || null,
10927 item = ItemSet.itemFromTarget(event);
10928
10929 if (item) {
10930 // update item
10931
10932 // execute async handler to update the item (or cancel it)
10933 var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
10934 this.options.onUpdate(itemData, function (itemData) {
10935 if (itemData) {
10936 me.itemsData.update(itemData);
10937 }
10938 });
10939 }
10940 else {
10941 // add item
10942 var xAbs = util.getAbsoluteLeft(this.dom.frame);
10943 var x = event.gesture.center.pageX - xAbs;
10944 var start = this.body.util.toTime(x);
10945 var newItem = {
10946 start: snap ? snap(start) : start,
10947 content: 'new item'
10948 };
10949
10950 // when default type is a range, add a default end date to the new item
10951 if (this.options.type === 'range') {
10952 var end = this.body.util.toTime(x + this.props.width / 5);
10953 newItem.end = snap ? snap(end) : end;
10954 }
10955
10956 newItem[this.itemsData._fieldId] = util.randomUUID();
10957
10958 var group = ItemSet.groupFromTarget(event);
10959 if (group) {
10960 newItem.group = group.groupId;
10961 }
10962
10963 // execute async handler to customize (or cancel) adding an item
10964 this.options.onAdd(newItem, function (item) {
10965 if (item) {
10966 me.itemsData.add(item);
10967 // TODO: need to trigger a redraw?
10968 }
10969 });
10970 }
10971 };
10972
10973 /**
10974 * Handle selecting/deselecting multiple items when holding an item
10975 * @param {Event} event
10976 * @private
10977 */
10978 ItemSet.prototype._onMultiSelectItem = function (event) {
10979 if (!this.options.selectable) return;
10980
10981 var selection,
10982 item = ItemSet.itemFromTarget(event);
10983
10984 if (item) {
10985 // multi select items
10986 selection = this.getSelection(); // current selection
10987 var index = selection.indexOf(item.id);
10988 if (index == -1) {
10989 // item is not yet selected -> select it
10990 selection.push(item.id);
10991 }
10992 else {
10993 // item is already selected -> deselect it
10994 selection.splice(index, 1);
10995 }
10996 this.setSelection(selection);
10997
10998 this.body.emitter.emit('select', {
10999 items: this.getSelection()
11000 });
11001
11002 event.stopPropagation();
11003 }
11004 };
11005
11006 /**
11007 * Find an item from an event target:
11008 * searches for the attribute 'timeline-item' in the event target's element tree
11009 * @param {Event} event
11010 * @return {Item | null} item
11011 */
11012 ItemSet.itemFromTarget = function(event) {
11013 var target = event.target;
11014 while (target) {
11015 if (target.hasOwnProperty('timeline-item')) {
11016 return target['timeline-item'];
11017 }
11018 target = target.parentNode;
11019 }
11020
11021 return null;
11022 };
11023
11024 /**
11025 * Find the Group from an event target:
11026 * searches for the attribute 'timeline-group' in the event target's element tree
11027 * @param {Event} event
11028 * @return {Group | null} group
11029 */
11030 ItemSet.groupFromTarget = function(event) {
11031 var target = event.target;
11032 while (target) {
11033 if (target.hasOwnProperty('timeline-group')) {
11034 return target['timeline-group'];
11035 }
11036 target = target.parentNode;
11037 }
11038
11039 return null;
11040 };
11041
11042 /**
11043 * Find the ItemSet from an event target:
11044 * searches for the attribute 'timeline-itemset' in the event target's element tree
11045 * @param {Event} event
11046 * @return {ItemSet | null} item
11047 */
11048 ItemSet.itemSetFromTarget = function(event) {
11049 var target = event.target;
11050 while (target) {
11051 if (target.hasOwnProperty('timeline-itemset')) {
11052 return target['timeline-itemset'];
11053 }
11054 target = target.parentNode;
11055 }
11056
11057 return null;
11058 };
11059
11060 module.exports = ItemSet;
11061
11062
11063/***/ },
11064/* 25 */
11065/***/ function(module, exports, __webpack_require__) {
11066
11067 var util = __webpack_require__(1);
11068 var DOMutil = __webpack_require__(2);
11069 var Component = __webpack_require__(18);
11070
11071 /**
11072 * Legend for Graph2d
11073 */
11074 function Legend(body, options, side, linegraphOptions) {
11075 this.body = body;
11076 this.defaultOptions = {
11077 enabled: true,
11078 icons: true,
11079 iconSize: 20,
11080 iconSpacing: 6,
11081 left: {
11082 visible: true,
11083 position: 'top-left' // top/bottom - left,center,right
11084 },
11085 right: {
11086 visible: true,
11087 position: 'top-left' // top/bottom - left,center,right
11088 }
11089 }
11090 this.side = side;
11091 this.options = util.extend({},this.defaultOptions);
11092 this.linegraphOptions = linegraphOptions;
11093
11094 this.svgElements = {};
11095 this.dom = {};
11096 this.groups = {};
11097 this.amountOfGroups = 0;
11098 this._create();
11099
11100 this.setOptions(options);
11101 }
11102
11103 Legend.prototype = new Component();
11104
11105
11106 Legend.prototype.addGroup = function(label, graphOptions) {
11107 if (!this.groups.hasOwnProperty(label)) {
11108 this.groups[label] = graphOptions;
11109 }
11110 this.amountOfGroups += 1;
11111 };
11112
11113 Legend.prototype.updateGroup = function(label, graphOptions) {
11114 this.groups[label] = graphOptions;
11115 };
11116
11117 Legend.prototype.removeGroup = function(label) {
11118 if (this.groups.hasOwnProperty(label)) {
11119 delete this.groups[label];
11120 this.amountOfGroups -= 1;
11121 }
11122 };
11123
11124 Legend.prototype._create = function() {
11125 this.dom.frame = document.createElement('div');
11126 this.dom.frame.className = 'legend';
11127 this.dom.frame.style.position = "absolute";
11128 this.dom.frame.style.top = "10px";
11129 this.dom.frame.style.display = "block";
11130
11131 this.dom.textArea = document.createElement('div');
11132 this.dom.textArea.className = 'legendText';
11133 this.dom.textArea.style.position = "relative";
11134 this.dom.textArea.style.top = "0px";
11135
11136 this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
11137 this.svg.style.position = 'absolute';
11138 this.svg.style.top = 0 +'px';
11139 this.svg.style.width = this.options.iconSize + 5 + 'px';
11140
11141 this.dom.frame.appendChild(this.svg);
11142 this.dom.frame.appendChild(this.dom.textArea);
11143 };
11144
11145 /**
11146 * Hide the component from the DOM
11147 */
11148 Legend.prototype.hide = function() {
11149 // remove the frame containing the items
11150 if (this.dom.frame.parentNode) {
11151 this.dom.frame.parentNode.removeChild(this.dom.frame);
11152 }
11153 };
11154
11155 /**
11156 * Show the component in the DOM (when not already visible).
11157 * @return {Boolean} changed
11158 */
11159 Legend.prototype.show = function() {
11160 // show frame containing the items
11161 if (!this.dom.frame.parentNode) {
11162 this.body.dom.center.appendChild(this.dom.frame);
11163 }
11164 };
11165
11166 Legend.prototype.setOptions = function(options) {
11167 var fields = ['enabled','orientation','icons','left','right'];
11168 util.selectiveDeepExtend(fields, this.options, options);
11169 };
11170
11171 Legend.prototype.redraw = function() {
11172 var activeGroups = 0;
11173 for (var groupId in this.groups) {
11174 if (this.groups.hasOwnProperty(groupId)) {
11175 if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
11176 activeGroups++;
11177 }
11178 }
11179 }
11180
11181 if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
11182 this.hide();
11183 }
11184 else {
11185 this.show();
11186 if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
11187 this.dom.frame.style.left = '4px';
11188 this.dom.frame.style.textAlign = "left";
11189 this.dom.textArea.style.textAlign = "left";
11190 this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
11191 this.dom.textArea.style.right = '';
11192 this.svg.style.left = 0 +'px';
11193 this.svg.style.right = '';
11194 }
11195 else {
11196 this.dom.frame.style.right = '4px';
11197 this.dom.frame.style.textAlign = "right";
11198 this.dom.textArea.style.textAlign = "right";
11199 this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
11200 this.dom.textArea.style.left = '';
11201 this.svg.style.right = 0 +'px';
11202 this.svg.style.left = '';
11203 }
11204
11205 if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
11206 this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
11207 this.dom.frame.style.bottom = '';
11208 }
11209 else {
11210 this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
11211 this.dom.frame.style.top = '';
11212 }
11213
11214 if (this.options.icons == false) {
11215 this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
11216 this.dom.textArea.style.right = '';
11217 this.dom.textArea.style.left = '';
11218 this.svg.style.width = '0px';
11219 }
11220 else {
11221 this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
11222 this.drawLegendIcons();
11223 }
11224
11225 var content = '';
11226 for (var groupId in this.groups) {
11227 if (this.groups.hasOwnProperty(groupId)) {
11228 if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
11229 content += this.groups[groupId].content + '<br />';
11230 }
11231 }
11232 }
11233 this.dom.textArea.innerHTML = content;
11234 this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
11235 }
11236 };
11237
11238 Legend.prototype.drawLegendIcons = function() {
11239 if (this.dom.frame.parentNode) {
11240 DOMutil.prepareElements(this.svgElements);
11241 var padding = window.getComputedStyle(this.dom.frame).paddingTop;
11242 var iconOffset = Number(padding.replace('px',''));
11243 var x = iconOffset;
11244 var iconWidth = this.options.iconSize;
11245 var iconHeight = 0.75 * this.options.iconSize;
11246 var y = iconOffset + 0.5 * iconHeight + 3;
11247
11248 this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
11249
11250 for (var groupId in this.groups) {
11251 if (this.groups.hasOwnProperty(groupId)) {
11252 if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
11253 this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
11254 y += iconHeight + this.options.iconSpacing;
11255 }
11256 }
11257 }
11258
11259 DOMutil.cleanupElements(this.svgElements);
11260 }
11261 };
11262
11263 module.exports = Legend;
11264
11265
11266/***/ },
11267/* 26 */
11268/***/ function(module, exports, __webpack_require__) {
11269
11270 var util = __webpack_require__(1);
11271 var DOMutil = __webpack_require__(2);
11272 var DataSet = __webpack_require__(3);
11273 var DataView = __webpack_require__(4);
11274 var Component = __webpack_require__(18);
11275 var DataAxis = __webpack_require__(21);
11276 var GraphGroup = __webpack_require__(22);
11277 var Legend = __webpack_require__(25);
11278
11279 var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
11280
11281 /**
11282 * This is the constructor of the LineGraph. It requires a Timeline body and options.
11283 *
11284 * @param body
11285 * @param options
11286 * @constructor
11287 */
11288 function LineGraph(body, options) {
11289 this.id = util.randomUUID();
11290 this.body = body;
11291
11292 this.defaultOptions = {
11293 yAxisOrientation: 'left',
11294 defaultGroup: 'default',
11295 sort: true,
11296 sampling: true,
11297 graphHeight: '400px',
11298 shaded: {
11299 enabled: false,
11300 orientation: 'bottom' // top, bottom
11301 },
11302 style: 'line', // line, bar
11303 barChart: {
11304 width: 50,
11305 handleOverlap: 'overlap',
11306 align: 'center' // left, center, right
11307 },
11308 catmullRom: {
11309 enabled: true,
11310 parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
11311 alpha: 0.5
11312 },
11313 drawPoints: {
11314 enabled: true,
11315 size: 6,
11316 style: 'square' // square, circle
11317 },
11318 dataAxis: {
11319 showMinorLabels: true,
11320 showMajorLabels: true,
11321 icons: false,
11322 width: '40px',
11323 visible: true,
11324 customRange: {
11325 left: {min:undefined, max:undefined},
11326 right: {min:undefined, max:undefined}
11327 }
11328 },
11329 legend: {
11330 enabled: false,
11331 icons: true,
11332 left: {
11333 visible: true,
11334 position: 'top-left' // top/bottom - left,right
11335 },
11336 right: {
11337 visible: true,
11338 position: 'top-right' // top/bottom - left,right
11339 }
11340 },
11341 groups: {
11342 visibility: {}
11343 }
11344 };
11345
11346 // options is shared by this ItemSet and all its items
11347 this.options = util.extend({}, this.defaultOptions);
11348 this.dom = {};
11349 this.props = {};
11350 this.hammer = null;
11351 this.groups = {};
11352 this.abortedGraphUpdate = false;
11353
11354 var me = this;
11355 this.itemsData = null; // DataSet
11356 this.groupsData = null; // DataSet
11357
11358 // listeners for the DataSet of the items
11359 this.itemListeners = {
11360 'add': function (event, params, senderId) {
11361 me._onAdd(params.items);
11362 },
11363 'update': function (event, params, senderId) {
11364 me._onUpdate(params.items);
11365 },
11366 'remove': function (event, params, senderId) {
11367 me._onRemove(params.items);
11368 }
11369 };
11370
11371 // listeners for the DataSet of the groups
11372 this.groupListeners = {
11373 'add': function (event, params, senderId) {
11374 me._onAddGroups(params.items);
11375 },
11376 'update': function (event, params, senderId) {
11377 me._onUpdateGroups(params.items);
11378 },
11379 'remove': function (event, params, senderId) {
11380 me._onRemoveGroups(params.items);
11381 }
11382 };
11383
11384 this.items = {}; // object with an Item for every data item
11385 this.selection = []; // list with the ids of all selected nodes
11386 this.lastStart = this.body.range.start;
11387 this.touchParams = {}; // stores properties while dragging
11388
11389 this.svgElements = {};
11390 this.setOptions(options);
11391 this.groupsUsingDefaultStyles = [0];
11392
11393 this.body.emitter.on("rangechanged", function() {
11394 me.lastStart = me.body.range.start;
11395 me.svg.style.left = util.option.asSize(-me.width);
11396 me._updateGraph.apply(me);
11397 });
11398
11399 // create the HTML DOM
11400 this._create();
11401 this.body.emitter.emit("change");
11402 }
11403
11404 LineGraph.prototype = new Component();
11405
11406 /**
11407 * Create the HTML DOM for the ItemSet
11408 */
11409 LineGraph.prototype._create = function(){
11410 var frame = document.createElement('div');
11411 frame.className = 'LineGraph';
11412 this.dom.frame = frame;
11413
11414 // create svg element for graph drawing.
11415 this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
11416 this.svg.style.position = "relative";
11417 this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
11418 this.svg.style.display = "block";
11419 frame.appendChild(this.svg);
11420
11421 // data axis
11422 this.options.dataAxis.orientation = 'left';
11423 this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
11424
11425 this.options.dataAxis.orientation = 'right';
11426 this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
11427 delete this.options.dataAxis.orientation;
11428
11429 // legends
11430 this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
11431 this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
11432
11433 this.show();
11434 };
11435
11436 /**
11437 * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
11438 * @param options
11439 */
11440 LineGraph.prototype.setOptions = function(options) {
11441 if (options) {
11442 var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
11443 util.selectiveDeepExtend(fields, this.options, options);
11444 util.mergeOptions(this.options, options,'catmullRom');
11445 util.mergeOptions(this.options, options,'drawPoints');
11446 util.mergeOptions(this.options, options,'shaded');
11447 util.mergeOptions(this.options, options,'legend');
11448
11449 if (options.catmullRom) {
11450 if (typeof options.catmullRom == 'object') {
11451 if (options.catmullRom.parametrization) {
11452 if (options.catmullRom.parametrization == 'uniform') {
11453 this.options.catmullRom.alpha = 0;
11454 }
11455 else if (options.catmullRom.parametrization == 'chordal') {
11456 this.options.catmullRom.alpha = 1.0;
11457 }
11458 else {
11459 this.options.catmullRom.parametrization = 'centripetal';
11460 this.options.catmullRom.alpha = 0.5;
11461 }
11462 }
11463 }
11464 }
11465
11466 if (this.yAxisLeft) {
11467 if (options.dataAxis !== undefined) {
11468 this.yAxisLeft.setOptions(this.options.dataAxis);
11469 this.yAxisRight.setOptions(this.options.dataAxis);
11470 }
11471 }
11472
11473 if (this.legendLeft) {
11474 if (options.legend !== undefined) {
11475 this.legendLeft.setOptions(this.options.legend);
11476 this.legendRight.setOptions(this.options.legend);
11477 }
11478 }
11479
11480 if (this.groups.hasOwnProperty(UNGROUPED)) {
11481 this.groups[UNGROUPED].setOptions(options);
11482 }
11483 }
11484 if (this.dom.frame) {
11485 this._updateGraph();
11486 }
11487 };
11488
11489 /**
11490 * Hide the component from the DOM
11491 */
11492 LineGraph.prototype.hide = function() {
11493 // remove the frame containing the items
11494 if (this.dom.frame.parentNode) {
11495 this.dom.frame.parentNode.removeChild(this.dom.frame);
11496 }
11497 };
11498
11499 /**
11500 * Show the component in the DOM (when not already visible).
11501 * @return {Boolean} changed
11502 */
11503 LineGraph.prototype.show = function() {
11504 // show frame containing the items
11505 if (!this.dom.frame.parentNode) {
11506 this.body.dom.center.appendChild(this.dom.frame);
11507 }
11508 };
11509
11510
11511 /**
11512 * Set items
11513 * @param {vis.DataSet | null} items
11514 */
11515 LineGraph.prototype.setItems = function(items) {
11516 var me = this,
11517 ids,
11518 oldItemsData = this.itemsData;
11519
11520 // replace the dataset
11521 if (!items) {
11522 this.itemsData = null;
11523 }
11524 else if (items instanceof DataSet || items instanceof DataView) {
11525 this.itemsData = items;
11526 }
11527 else {
11528 throw new TypeError('Data must be an instance of DataSet or DataView');
11529 }
11530
11531 if (oldItemsData) {
11532 // unsubscribe from old dataset
11533 util.forEach(this.itemListeners, function (callback, event) {
11534 oldItemsData.off(event, callback);
11535 });
11536
11537 // remove all drawn items
11538 ids = oldItemsData.getIds();
11539 this._onRemove(ids);
11540 }
11541
11542 if (this.itemsData) {
11543 // subscribe to new dataset
11544 var id = this.id;
11545 util.forEach(this.itemListeners, function (callback, event) {
11546 me.itemsData.on(event, callback, id);
11547 });
11548
11549 // add all new items
11550 ids = this.itemsData.getIds();
11551 this._onAdd(ids);
11552 }
11553 this._updateUngrouped();
11554 this._updateGraph();
11555 this.redraw();
11556 };
11557
11558 /**
11559 * Set groups
11560 * @param {vis.DataSet} groups
11561 */
11562 LineGraph.prototype.setGroups = function(groups) {
11563 var me = this,
11564 ids;
11565
11566 // unsubscribe from current dataset
11567 if (this.groupsData) {
11568 util.forEach(this.groupListeners, function (callback, event) {
11569 me.groupsData.unsubscribe(event, callback);
11570 });
11571
11572 // remove all drawn groups
11573 ids = this.groupsData.getIds();
11574 this.groupsData = null;
11575 this._onRemoveGroups(ids); // note: this will cause a redraw
11576 }
11577
11578 // replace the dataset
11579 if (!groups) {
11580 this.groupsData = null;
11581 }
11582 else if (groups instanceof DataSet || groups instanceof DataView) {
11583 this.groupsData = groups;
11584 }
11585 else {
11586 throw new TypeError('Data must be an instance of DataSet or DataView');
11587 }
11588
11589 if (this.groupsData) {
11590 // subscribe to new dataset
11591 var id = this.id;
11592 util.forEach(this.groupListeners, function (callback, event) {
11593 me.groupsData.on(event, callback, id);
11594 });
11595
11596 // draw all ms
11597 ids = this.groupsData.getIds();
11598 this._onAddGroups(ids);
11599 }
11600 this._onUpdate();
11601 };
11602
11603
11604 /**
11605 * Update the datapoints
11606 * @param [ids]
11607 * @private
11608 */
11609 LineGraph.prototype._onUpdate = function(ids) {
11610 this._updateUngrouped();
11611 this._updateAllGroupData();
11612 this._updateGraph();
11613 this.redraw();
11614 };
11615 LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
11616 LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
11617 LineGraph.prototype._onUpdateGroups = function (groupIds) {
11618 for (var i = 0; i < groupIds.length; i++) {
11619 var group = this.groupsData.get(groupIds[i]);
11620 this._updateGroup(group, groupIds[i]);
11621 }
11622
11623 this._updateGraph();
11624 this.redraw();
11625 };
11626 LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
11627
11628 LineGraph.prototype._onRemoveGroups = function (groupIds) {
11629 for (var i = 0; i < groupIds.length; i++) {
11630 if (!this.groups.hasOwnProperty(groupIds[i])) {
11631 if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
11632 this.yAxisRight.removeGroup(groupIds[i]);
11633 this.legendRight.removeGroup(groupIds[i]);
11634 this.legendRight.redraw();
11635 }
11636 else {
11637 this.yAxisLeft.removeGroup(groupIds[i]);
11638 this.legendLeft.removeGroup(groupIds[i]);
11639 this.legendLeft.redraw();
11640 }
11641 delete this.groups[groupIds[i]];
11642 }
11643 }
11644 this._updateUngrouped();
11645 this._updateGraph();
11646 this.redraw();
11647 };
11648
11649 /**
11650 * update a group object
11651 *
11652 * @param group
11653 * @param groupId
11654 * @private
11655 */
11656 LineGraph.prototype._updateGroup = function (group, groupId) {
11657 if (!this.groups.hasOwnProperty(groupId)) {
11658 this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
11659 if (this.groups[groupId].options.yAxisOrientation == 'right') {
11660 this.yAxisRight.addGroup(groupId, this.groups[groupId]);
11661 this.legendRight.addGroup(groupId, this.groups[groupId]);
11662 }
11663 else {
11664 this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
11665 this.legendLeft.addGroup(groupId, this.groups[groupId]);
11666 }
11667 }
11668 else {
11669 this.groups[groupId].update(group);
11670 if (this.groups[groupId].options.yAxisOrientation == 'right') {
11671 this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
11672 this.legendRight.updateGroup(groupId, this.groups[groupId]);
11673 }
11674 else {
11675 this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
11676 this.legendLeft.updateGroup(groupId, this.groups[groupId]);
11677 }
11678 }
11679 this.legendLeft.redraw();
11680 this.legendRight.redraw();
11681 };
11682
11683 LineGraph.prototype._updateAllGroupData = function () {
11684 if (this.itemsData != null) {
11685 var groupsContent = {};
11686 var groupId;
11687 for (groupId in this.groups) {
11688 if (this.groups.hasOwnProperty(groupId)) {
11689 groupsContent[groupId] = [];
11690 }
11691 }
11692 for (var itemId in this.itemsData._data) {
11693 if (this.itemsData._data.hasOwnProperty(itemId)) {
11694 var item = this.itemsData._data[itemId];
11695 item.x = util.convert(item.x,"Date");
11696 groupsContent[item.group].push(item);
11697 }
11698 }
11699 for (groupId in this.groups) {
11700 if (this.groups.hasOwnProperty(groupId)) {
11701 this.groups[groupId].setItems(groupsContent[groupId]);
11702 }
11703 }
11704 }
11705 };
11706
11707 /**
11708 * Create or delete the group holding all ungrouped items. This group is used when
11709 * there are no groups specified. This anonymous group is called 'graph'.
11710 * @protected
11711 */
11712 LineGraph.prototype._updateUngrouped = function() {
11713 if (this.itemsData && this.itemsData != null) {
11714 var ungroupedCounter = 0;
11715 for (var itemId in this.itemsData._data) {
11716 if (this.itemsData._data.hasOwnProperty(itemId)) {
11717 var item = this.itemsData._data[itemId];
11718 if (item != undefined) {
11719 if (item.hasOwnProperty('group')) {
11720 if (item.group === undefined) {
11721 item.group = UNGROUPED;
11722 }
11723 }
11724 else {
11725 item.group = UNGROUPED;
11726 }
11727 ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
11728 }
11729 }
11730 }
11731
11732 if (ungroupedCounter == 0) {
11733 delete this.groups[UNGROUPED];
11734 this.legendLeft.removeGroup(UNGROUPED);
11735 this.legendRight.removeGroup(UNGROUPED);
11736 this.yAxisLeft.removeGroup(UNGROUPED);
11737 this.yAxisRight.removeGroup(UNGROUPED);
11738 }
11739 else {
11740 var group = {id: UNGROUPED, content: this.options.defaultGroup};
11741 this._updateGroup(group, UNGROUPED);
11742 }
11743 }
11744 else {
11745 delete this.groups[UNGROUPED];
11746 this.legendLeft.removeGroup(UNGROUPED);
11747 this.legendRight.removeGroup(UNGROUPED);
11748 this.yAxisLeft.removeGroup(UNGROUPED);
11749 this.yAxisRight.removeGroup(UNGROUPED);
11750 }
11751
11752 this.legendLeft.redraw();
11753 this.legendRight.redraw();
11754 };
11755
11756
11757 /**
11758 * Redraw the component, mandatory function
11759 * @return {boolean} Returns true if the component is resized
11760 */
11761 LineGraph.prototype.redraw = function() {
11762 var resized = false;
11763
11764 this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
11765 if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
11766 resized = true;
11767 }
11768 // check if this component is resized
11769 resized = this._isResized() || resized;
11770 // check whether zoomed (in that case we need to re-stack everything)
11771 var visibleInterval = this.body.range.end - this.body.range.start;
11772 var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
11773 this.lastVisibleInterval = visibleInterval;
11774 this.lastWidth = this.width;
11775
11776 // calculate actual size and position
11777 this.width = this.dom.frame.offsetWidth;
11778
11779 // the svg element is three times as big as the width, this allows for fully dragging left and right
11780 // without reloading the graph. the controls for this are bound to events in the constructor
11781 if (resized == true) {
11782 this.svg.style.width = util.option.asSize(3*this.width);
11783 this.svg.style.left = util.option.asSize(-this.width);
11784 }
11785
11786 if (zoomed == true || this.abortedGraphUpdate == true) {
11787 this._updateGraph();
11788 }
11789 else {
11790 // move the whole svg while dragging
11791 if (this.lastStart != 0) {
11792 var offset = this.body.range.start - this.lastStart;
11793 var range = this.body.range.end - this.body.range.start;
11794 if (this.width != 0) {
11795 var rangePerPixelInv = this.width/range;
11796 var xOffset = offset * rangePerPixelInv;
11797 this.svg.style.left = (-this.width - xOffset) + "px";
11798 }
11799 }
11800
11801 }
11802
11803 this.legendLeft.redraw();
11804 this.legendRight.redraw();
11805
11806 return resized;
11807 };
11808
11809 /**
11810 * Update and redraw the graph.
11811 *
11812 */
11813 LineGraph.prototype._updateGraph = function () {
11814 // reset the svg elements
11815 DOMutil.prepareElements(this.svgElements);
11816 if (this.width != 0 && this.itemsData != null) {
11817 var group, i;
11818 var preprocessedGroupData = {};
11819 var processedGroupData = {};
11820 var groupRanges = {};
11821 var changeCalled = false;
11822
11823 // getting group Ids
11824 var groupIds = [];
11825 for (var groupId in this.groups) {
11826 if (this.groups.hasOwnProperty(groupId)) {
11827 group = this.groups[groupId];
11828 if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
11829 groupIds.push(groupId);
11830 }
11831 }
11832 }
11833 if (groupIds.length > 0) {
11834 // this is the range of the SVG canvas
11835 var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
11836 var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
11837 var groupsData = {};
11838 // fill groups data
11839 this._getRelevantData(groupIds, groupsData, minDate, maxDate);
11840 // we transform the X coordinates to detect collisions
11841 for (i = 0; i < groupIds.length; i++) {
11842 preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
11843 }
11844 // now all needed data has been collected we start the processing.
11845 this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
11846
11847 // update the Y axis first, we use this data to draw at the correct Y points
11848 // changeCalled is required to clean the SVG on a change emit.
11849 changeCalled = this._updateYAxis(groupIds, groupRanges);
11850 if (changeCalled == true) {
11851 DOMutil.cleanupElements(this.svgElements);
11852 this.abortedGraphUpdate = true;
11853 this.body.emitter.emit("change");
11854 return;
11855 }
11856 this.abortedGraphUpdate = false;
11857
11858 // With the yAxis scaled correctly, use this to get the Y values of the points.
11859 for (i = 0; i < groupIds.length; i++) {
11860 group = this.groups[groupIds[i]];
11861 processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
11862 }
11863
11864
11865 // draw the groups
11866 for (i = 0; i < groupIds.length; i++) {
11867 group = this.groups[groupIds[i]];
11868 if (group.options.style == 'line') {
11869 this._drawLineGraph(processedGroupData[groupIds[i]], group);
11870 }
11871 }
11872 this._drawBarGraphs(groupIds, processedGroupData);
11873 }
11874 }
11875
11876 // cleanup unused svg elements
11877 DOMutil.cleanupElements(this.svgElements);
11878 };
11879
11880
11881 LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
11882 // first select and preprocess the data from the datasets.
11883 // the groups have their preselection of data, we now loop over this data to see
11884 // what data we need to draw. Sorted data is much faster.
11885 // more optimization is possible by doing the sampling before and using the binary search
11886 // to find the end date to determine the increment.
11887 var group, i, j, item;
11888 if (groupIds.length > 0) {
11889 for (i = 0; i < groupIds.length; i++) {
11890 group = this.groups[groupIds[i]];
11891 groupsData[groupIds[i]] = [];
11892 var dataContainer = groupsData[groupIds[i]];
11893 // optimization for sorted data
11894 if (group.options.sort == true) {
11895 var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
11896 for (j = guess; j < group.itemsData.length; j++) {
11897 item = group.itemsData[j];
11898 if (item !== undefined) {
11899 if (item.x > maxDate) {
11900 dataContainer.push(item);
11901 break;
11902 }
11903 else {
11904 dataContainer.push(item);
11905 }
11906 }
11907 }
11908 }
11909 else {
11910 for (j = 0; j < group.itemsData.length; j++) {
11911 item = group.itemsData[j];
11912 if (item !== undefined) {
11913 if (item.x > minDate && item.x < maxDate) {
11914 dataContainer.push(item);
11915 }
11916 }
11917 }
11918 }
11919 }
11920 }
11921
11922 this._applySampling(groupIds, groupsData);
11923 };
11924
11925 LineGraph.prototype._applySampling = function (groupIds, groupsData) {
11926 var group;
11927 if (groupIds.length > 0) {
11928 for (var i = 0; i < groupIds.length; i++) {
11929 group = this.groups[groupIds[i]];
11930 if (group.options.sampling == true) {
11931 var dataContainer = groupsData[groupIds[i]];
11932 if (dataContainer.length > 0) {
11933 var increment = 1;
11934 var amountOfPoints = dataContainer.length;
11935
11936 // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
11937 // of width changing of the yAxis.
11938 var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
11939 var pointsPerPixel = amountOfPoints / xDistance;
11940 increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
11941
11942 var sampledData = [];
11943 for (var j = 0; j < amountOfPoints; j += increment) {
11944 sampledData.push(dataContainer[j]);
11945
11946 }
11947 groupsData[groupIds[i]] = sampledData;
11948 }
11949 }
11950 }
11951 }
11952 };
11953
11954 LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
11955 var groupData, group, i,j;
11956 var barCombinedDataLeft = [];
11957 var barCombinedDataRight = [];
11958 var barCombinedData;
11959 if (groupIds.length > 0) {
11960 for (i = 0; i < groupIds.length; i++) {
11961 groupData = groupsData[groupIds[i]];
11962 if (groupData.length > 0) {
11963 group = this.groups[groupIds[i]];
11964 if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") {
11965 var yMin = groupData[0].y;
11966 var yMax = groupData[0].y;
11967 for (j = 0; j < groupData.length; j++) {
11968 yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
11969 yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
11970 }
11971 groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
11972 }
11973 else if (group.options.style == 'bar') {
11974 if (group.options.yAxisOrientation == 'left') {
11975 barCombinedData = barCombinedDataLeft;
11976 }
11977 else {
11978 barCombinedData = barCombinedDataRight;
11979 }
11980
11981 groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
11982
11983 // combine data
11984 for (j = 0; j < groupData.length; j++) {
11985 barCombinedData.push({
11986 x: groupData[j].x,
11987 y: groupData[j].y,
11988 groupId: groupIds[i]
11989 });
11990 }
11991 }
11992 }
11993 }
11994
11995 var intersections;
11996 if (barCombinedDataLeft.length > 0) {
11997 // sort by time and by group
11998 barCombinedDataLeft.sort(function (a, b) {
11999 if (a.x == b.x) {
12000 return a.groupId - b.groupId;
12001 } else {
12002 return a.x - b.x;
12003 }
12004 });
12005 intersections = {};
12006 this._getDataIntersections(intersections, barCombinedDataLeft);
12007 groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
12008 groupRanges["__barchartLeft"].yAxisOrientation = "left";
12009 groupIds.push("__barchartLeft");
12010 }
12011 if (barCombinedDataRight.length > 0) {
12012 // sort by time and by group
12013 barCombinedDataRight.sort(function (a, b) {
12014 if (a.x == b.x) {
12015 return a.groupId - b.groupId;
12016 } else {
12017 return a.x - b.x;
12018 }
12019 });
12020 intersections = {};
12021 this._getDataIntersections(intersections, barCombinedDataRight);
12022 groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight);
12023 groupRanges["__barchartRight"].yAxisOrientation = "right";
12024 groupIds.push("__barchartRight");
12025 }
12026 }
12027 };
12028
12029 LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
12030 var key;
12031 var yMin = combinedData[0].y;
12032 var yMax = combinedData[0].y;
12033 for (var i = 0; i < combinedData.length; i++) {
12034 key = combinedData[i].x;
12035 if (intersections[key] === undefined) {
12036 yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
12037 yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
12038 }
12039 else {
12040 intersections[key].accumulated += combinedData[i].y;
12041 }
12042 }
12043 for (var xpos in intersections) {
12044 if (intersections.hasOwnProperty(xpos)) {
12045 yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
12046 yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
12047 }
12048 }
12049
12050 return {min: yMin, max: yMax};
12051 };
12052
12053
12054 /**
12055 * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
12056 * @param {Array} groupIds
12057 * @param {Object} groupRanges
12058 * @private
12059 */
12060 LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
12061 var changeCalled = false;
12062 var yAxisLeftUsed = false;
12063 var yAxisRightUsed = false;
12064 var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
12065 // if groups are present
12066 if (groupIds.length > 0) {
12067 for (var i = 0; i < groupIds.length; i++) {
12068 if (groupRanges.hasOwnProperty(groupIds[i])) {
12069 if (groupRanges[groupIds[i]].ignore !== true) {
12070 minVal = groupRanges[groupIds[i]].min;
12071 maxVal = groupRanges[groupIds[i]].max;
12072
12073 if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
12074 yAxisLeftUsed = true;
12075 minLeft = minLeft > minVal ? minVal : minLeft;
12076 maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
12077 }
12078 else {
12079 yAxisRightUsed = true;
12080 minRight = minRight > minVal ? minVal : minRight;
12081 maxRight = maxRight < maxVal ? maxVal : maxRight;
12082 }
12083 }
12084 }
12085 }
12086
12087 if (yAxisLeftUsed == true) {
12088 this.yAxisLeft.setRange(minLeft, maxLeft);
12089 }
12090 if (yAxisRightUsed == true) {
12091 this.yAxisRight.setRange(minRight, maxRight);
12092 }
12093 }
12094
12095 changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
12096 changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
12097
12098 if (yAxisRightUsed == true && yAxisLeftUsed == true) {
12099 this.yAxisLeft.drawIcons = true;
12100 this.yAxisRight.drawIcons = true;
12101 }
12102 else {
12103 this.yAxisLeft.drawIcons = false;
12104 this.yAxisRight.drawIcons = false;
12105 }
12106
12107 this.yAxisRight.master = !yAxisLeftUsed;
12108
12109 if (this.yAxisRight.master == false) {
12110 if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
12111 else {this.yAxisLeft.lineOffset = 0;}
12112
12113 changeCalled = this.yAxisLeft.redraw() || changeCalled;
12114 this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
12115 changeCalled = this.yAxisRight.redraw() || changeCalled;
12116 }
12117 else {
12118 changeCalled = this.yAxisRight.redraw() || changeCalled;
12119 }
12120
12121 // clean the accumulated lists
12122 if (groupIds.indexOf("__barchartLeft") != -1) {
12123 groupIds.splice(groupIds.indexOf("__barchartLeft"),1);
12124 }
12125 if (groupIds.indexOf("__barchartRight") != -1) {
12126 groupIds.splice(groupIds.indexOf("__barchartRight"),1);
12127 }
12128
12129 return changeCalled;
12130 };
12131
12132 /**
12133 * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
12134 *
12135 * @param {boolean} axisUsed
12136 * @returns {boolean}
12137 * @private
12138 * @param axis
12139 */
12140 LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
12141 var changed = false;
12142 if (axisUsed == false) {
12143 if (axis.dom.frame.parentNode) {
12144 axis.hide();
12145 changed = true;
12146 }
12147 }
12148 else {
12149 if (!axis.dom.frame.parentNode) {
12150 axis.show();
12151 changed = true;
12152 }
12153 }
12154 return changed;
12155 };
12156
12157
12158 /**
12159 * draw a bar graph
12160 *
12161 * @param groupIds
12162 * @param processedGroupData
12163 */
12164 LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
12165 var combinedData = [];
12166 var intersections = {};
12167 var coreDistance;
12168 var key, drawData;
12169 var group;
12170 var i,j;
12171 var barPoints = 0;
12172
12173 // combine all barchart data
12174 for (i = 0; i < groupIds.length; i++) {
12175 group = this.groups[groupIds[i]];
12176 if (group.options.style == 'bar') {
12177 if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
12178 for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
12179 combinedData.push({
12180 x: processedGroupData[groupIds[i]][j].x,
12181 y: processedGroupData[groupIds[i]][j].y,
12182 groupId: groupIds[i]
12183 });
12184 barPoints += 1;
12185 }
12186 }
12187 }
12188 }
12189
12190 if (barPoints == 0) {return;}
12191
12192 // sort by time and by group
12193 combinedData.sort(function (a, b) {
12194 if (a.x == b.x) {
12195 return a.groupId - b.groupId;
12196 } else {
12197 return a.x - b.x;
12198 }
12199 });
12200
12201 // get intersections
12202 this._getDataIntersections(intersections, combinedData);
12203
12204 // plot barchart
12205 for (i = 0; i < combinedData.length; i++) {
12206 group = this.groups[combinedData[i].groupId];
12207 var minWidth = 0.1 * group.options.barChart.width;
12208
12209 key = combinedData[i].x;
12210 var heightOffset = 0;
12211 if (intersections[key] === undefined) {
12212 if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
12213 if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
12214 drawData = this._getSafeDrawData(coreDistance, group, minWidth);
12215 }
12216 else {
12217 var nextKey = i + (intersections[key].amount - intersections[key].resolved);
12218 var prevKey = i - (intersections[key].resolved + 1);
12219 if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
12220 if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
12221 drawData = this._getSafeDrawData(coreDistance, group, minWidth);
12222 intersections[key].resolved += 1;
12223
12224 if (group.options.barChart.handleOverlap == 'stack') {
12225 heightOffset = intersections[key].accumulated;
12226 intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
12227 }
12228 else if (group.options.barChart.handleOverlap == 'sideBySide') {
12229 drawData.width = drawData.width / intersections[key].amount;
12230 drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
12231 if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
12232 else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
12233 }
12234 }
12235 DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
12236 // draw points
12237 if (group.options.drawPoints.enabled == true) {
12238 DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
12239 }
12240 }
12241 };
12242
12243 /**
12244 * Fill the intersections object with counters of how many datapoints share the same x coordinates
12245 * @param intersections
12246 * @param combinedData
12247 * @private
12248 */
12249 LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
12250 // get intersections
12251 var coreDistance;
12252 for (var i = 0; i < combinedData.length; i++) {
12253 if (i + 1 < combinedData.length) {
12254 coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
12255 }
12256 if (i > 0) {
12257 coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
12258 }
12259 if (coreDistance == 0) {
12260 if (intersections[combinedData[i].x] === undefined) {
12261 intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
12262 }
12263 intersections[combinedData[i].x].amount += 1;
12264 }
12265 }
12266 };
12267
12268 /**
12269 * Get the width and offset for bargraphs based on the coredistance between datapoints
12270 *
12271 * @param coreDistance
12272 * @param group
12273 * @param minWidth
12274 * @returns {{width: Number, offset: Number}}
12275 * @private
12276 */
12277 LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
12278 var width, offset;
12279 if (coreDistance < group.options.barChart.width && coreDistance > 0) {
12280 width = coreDistance < minWidth ? minWidth : coreDistance;
12281
12282 offset = 0; // recalculate offset with the new width;
12283 if (group.options.barChart.align == 'left') {
12284 offset -= 0.5 * coreDistance;
12285 }
12286 else if (group.options.barChart.align == 'right') {
12287 offset += 0.5 * coreDistance;
12288 }
12289 }
12290 else {
12291 // default settings
12292 width = group.options.barChart.width;
12293 offset = 0;
12294 if (group.options.barChart.align == 'left') {
12295 offset -= 0.5 * group.options.barChart.width;
12296 }
12297 else if (group.options.barChart.align == 'right') {
12298 offset += 0.5 * group.options.barChart.width;
12299 }
12300 }
12301
12302 return {width: width, offset: offset};
12303 };
12304
12305
12306 /**
12307 * draw a line graph
12308 *
12309 * @param dataset
12310 * @param group
12311 */
12312 LineGraph.prototype._drawLineGraph = function (dataset, group) {
12313 if (dataset != null) {
12314 if (dataset.length > 0) {
12315 var path, d;
12316 var svgHeight = Number(this.svg.style.height.replace("px",""));
12317 path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
12318 path.setAttributeNS(null, "class", group.className);
12319
12320 // construct path from dataset
12321 if (group.options.catmullRom.enabled == true) {
12322 d = this._catmullRom(dataset, group);
12323 }
12324 else {
12325 d = this._linear(dataset);
12326 }
12327
12328 // append with points for fill and finalize the path
12329 if (group.options.shaded.enabled == true) {
12330 var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
12331 var dFill;
12332 if (group.options.shaded.orientation == 'top') {
12333 dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
12334 }
12335 else {
12336 dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
12337 }
12338 fillPath.setAttributeNS(null, "class", group.className + " fill");
12339 fillPath.setAttributeNS(null, "d", dFill);
12340 }
12341 // copy properties to path for drawing.
12342 path.setAttributeNS(null, "d", "M" + d);
12343
12344 // draw points
12345 if (group.options.drawPoints.enabled == true) {
12346 this._drawPoints(dataset, group, this.svgElements, this.svg);
12347 }
12348 }
12349 }
12350 };
12351
12352 /**
12353 * draw the data points
12354 *
12355 * @param {Array} dataset
12356 * @param {Object} JSONcontainer
12357 * @param {Object} svg | SVG DOM element
12358 * @param {GraphGroup} group
12359 * @param {Number} [offset]
12360 */
12361 LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
12362 if (offset === undefined) {offset = 0;}
12363 for (var i = 0; i < dataset.length; i++) {
12364 DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
12365 }
12366 };
12367
12368
12369
12370 /**
12371 * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
12372 * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
12373 * the yAxis.
12374 *
12375 * @param datapoints
12376 * @returns {Array}
12377 * @private
12378 */
12379 LineGraph.prototype._convertXcoordinates = function (datapoints) {
12380 var extractedData = [];
12381 var xValue, yValue;
12382 var toScreen = this.body.util.toScreen;
12383
12384 for (var i = 0; i < datapoints.length; i++) {
12385 xValue = toScreen(datapoints[i].x) + this.width;
12386 yValue = datapoints[i].y;
12387 extractedData.push({x: xValue, y: yValue});
12388 }
12389
12390 return extractedData;
12391 };
12392
12393
12394
12395 /**
12396 * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
12397 * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
12398 * the yAxis.
12399 *
12400 * @param datapoints
12401 * @returns {Array}
12402 * @private
12403 */
12404 LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
12405 var extractedData = [];
12406 var xValue, yValue;
12407 var toScreen = this.body.util.toScreen;
12408 var axis = this.yAxisLeft;
12409 var svgHeight = Number(this.svg.style.height.replace("px",""));
12410 if (group.options.yAxisOrientation == 'right') {
12411 axis = this.yAxisRight;
12412 }
12413
12414 for (var i = 0; i < datapoints.length; i++) {
12415 xValue = toScreen(datapoints[i].x) + this.width;
12416 yValue = Math.round(axis.convertValue(datapoints[i].y));
12417 extractedData.push({x: xValue, y: yValue});
12418 }
12419
12420 group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
12421
12422 return extractedData;
12423 };
12424
12425 /**
12426 * This uses an uniform parametrization of the CatmullRom algorithm:
12427 * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
12428 * @param data
12429 * @returns {string}
12430 * @private
12431 */
12432 LineGraph.prototype._catmullRomUniform = function(data) {
12433 // catmull rom
12434 var p0, p1, p2, p3, bp1, bp2;
12435 var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
12436 var normalization = 1/6;
12437 var length = data.length;
12438 for (var i = 0; i < length - 1; i++) {
12439
12440 p0 = (i == 0) ? data[0] : data[i-1];
12441 p1 = data[i];
12442 p2 = data[i+1];
12443 p3 = (i + 2 < length) ? data[i+2] : p2;
12444
12445
12446 // Catmull-Rom to Cubic Bezier conversion matrix
12447 // 0 1 0 0
12448 // -1/6 1 1/6 0
12449 // 0 1/6 1 -1/6
12450 // 0 0 1 0
12451
12452 // bp0 = { x: p1.x, y: p1.y };
12453 bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
12454 bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
12455 // bp0 = { x: p2.x, y: p2.y };
12456
12457 d += "C" +
12458 bp1.x + "," +
12459 bp1.y + " " +
12460 bp2.x + "," +
12461 bp2.y + " " +
12462 p2.x + "," +
12463 p2.y + " ";
12464 }
12465
12466 return d;
12467 };
12468
12469 /**
12470 * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
12471 * By default, the centripetal parameterization is used because this gives the nicest results.
12472 * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
12473 *
12474 * One optimization can be used to reuse distances since this is a sliding window approach.
12475 * @param data
12476 * @returns {string}
12477 * @private
12478 */
12479 LineGraph.prototype._catmullRom = function(data, group) {
12480 var alpha = group.options.catmullRom.alpha;
12481 if (alpha == 0 || alpha === undefined) {
12482 return this._catmullRomUniform(data);
12483 }
12484 else {
12485 var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
12486 var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
12487 var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
12488 var length = data.length;
12489 for (var i = 0; i < length - 1; i++) {
12490
12491 p0 = (i == 0) ? data[0] : data[i-1];
12492 p1 = data[i];
12493 p2 = data[i+1];
12494 p3 = (i + 2 < length) ? data[i+2] : p2;
12495
12496 d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
12497 d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
12498 d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
12499
12500 // Catmull-Rom to Cubic Bezier conversion matrix
12501 //
12502 // A = 2d1^2a + 3d1^a * d2^a + d3^2a
12503 // B = 2d3^2a + 3d3^a * d2^a + d2^2a
12504 //
12505 // [ 0 1 0 0 ]
12506 // [ -d2^2a/N A/N d1^2a/N 0 ]
12507 // [ 0 d3^2a/M B/M -d2^2a/M ]
12508 // [ 0 0 1 0 ]
12509
12510 // [ 0 1 0 0 ]
12511 // [ -d2pow2a/N A/N d1pow2a/N 0 ]
12512 // [ 0 d3pow2a/M B/M -d2pow2a/M ]
12513 // [ 0 0 1 0 ]
12514
12515 d3powA = Math.pow(d3, alpha);
12516 d3pow2A = Math.pow(d3,2*alpha);
12517 d2powA = Math.pow(d2, alpha);
12518 d2pow2A = Math.pow(d2,2*alpha);
12519 d1powA = Math.pow(d1, alpha);
12520 d1pow2A = Math.pow(d1,2*alpha);
12521
12522 A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
12523 B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
12524 N = 3*d1powA * (d1powA + d2powA);
12525 if (N > 0) {N = 1 / N;}
12526 M = 3*d3powA * (d3powA + d2powA);
12527 if (M > 0) {M = 1 / M;}
12528
12529 bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
12530 y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
12531
12532 bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
12533 y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
12534
12535 if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
12536 if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
12537 d += "C" +
12538 bp1.x + "," +
12539 bp1.y + " " +
12540 bp2.x + "," +
12541 bp2.y + " " +
12542 p2.x + "," +
12543 p2.y + " ";
12544 }
12545
12546 return d;
12547 }
12548 };
12549
12550 /**
12551 * this generates the SVG path for a linear drawing between datapoints.
12552 * @param data
12553 * @returns {string}
12554 * @private
12555 */
12556 LineGraph.prototype._linear = function(data) {
12557 // linear
12558 var d = "";
12559 for (var i = 0; i < data.length; i++) {
12560 if (i == 0) {
12561 d += data[i].x + "," + data[i].y;
12562 }
12563 else {
12564 d += " " + data[i].x + "," + data[i].y;
12565 }
12566 }
12567 return d;
12568 };
12569
12570 module.exports = LineGraph;
12571
12572
12573/***/ },
12574/* 27 */
12575/***/ function(module, exports, __webpack_require__) {
12576
12577 var util = __webpack_require__(1);
12578 var Component = __webpack_require__(18);
12579 var TimeStep = __webpack_require__(17);
12580 var moment = __webpack_require__(41);
12581
12582 /**
12583 * A horizontal time axis
12584 * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
12585 * @param {Object} [options] See TimeAxis.setOptions for the available
12586 * options.
12587 * @constructor TimeAxis
12588 * @extends Component
12589 */
12590 function TimeAxis (body, options) {
12591 this.dom = {
12592 foreground: null,
12593 majorLines: [],
12594 majorTexts: [],
12595 minorLines: [],
12596 minorTexts: [],
12597 redundant: {
12598 majorLines: [],
12599 majorTexts: [],
12600 minorLines: [],
12601 minorTexts: []
12602 }
12603 };
12604 this.props = {
12605 range: {
12606 start: 0,
12607 end: 0,
12608 minimumStep: 0
12609 },
12610 lineTop: 0
12611 };
12612
12613 this.defaultOptions = {
12614 orientation: 'bottom', // supported: 'top', 'bottom'
12615 // TODO: implement timeaxis orientations 'left' and 'right'
12616 showMinorLabels: true,
12617 showMajorLabels: true
12618 };
12619 this.options = util.extend({}, this.defaultOptions);
12620
12621 this.body = body;
12622
12623 // create the HTML DOM
12624 this._create();
12625
12626 this.setOptions(options);
12627 }
12628
12629 TimeAxis.prototype = new Component();
12630
12631 /**
12632 * Set options for the TimeAxis.
12633 * Parameters will be merged in current options.
12634 * @param {Object} options Available options:
12635 * {string} [orientation]
12636 * {boolean} [showMinorLabels]
12637 * {boolean} [showMajorLabels]
12638 */
12639 TimeAxis.prototype.setOptions = function(options) {
12640 if (options) {
12641 // copy all options that we know
12642 util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
12643
12644 // apply locale to moment.js
12645 // TODO: not so nice, this is applied globally to moment.js
12646 if ('locale' in options) {
12647 if (typeof moment.locale === 'function') {
12648 // moment.js 2.8.1+
12649 moment.locale(options.locale);
12650 }
12651 else {
12652 moment.lang(options.locale);
12653 }
12654 }
12655 }
12656 };
12657
12658 /**
12659 * Create the HTML DOM for the TimeAxis
12660 */
12661 TimeAxis.prototype._create = function() {
12662 this.dom.foreground = document.createElement('div');
12663 this.dom.background = document.createElement('div');
12664
12665 this.dom.foreground.className = 'timeaxis foreground';
12666 this.dom.background.className = 'timeaxis background';
12667 };
12668
12669 /**
12670 * Destroy the TimeAxis
12671 */
12672 TimeAxis.prototype.destroy = function() {
12673 // remove from DOM
12674 if (this.dom.foreground.parentNode) {
12675 this.dom.foreground.parentNode.removeChild(this.dom.foreground);
12676 }
12677 if (this.dom.background.parentNode) {
12678 this.dom.background.parentNode.removeChild(this.dom.background);
12679 }
12680
12681 this.body = null;
12682 };
12683
12684 /**
12685 * Repaint the component
12686 * @return {boolean} Returns true if the component is resized
12687 */
12688 TimeAxis.prototype.redraw = function () {
12689 var options = this.options,
12690 props = this.props,
12691 foreground = this.dom.foreground,
12692 background = this.dom.background;
12693
12694 // determine the correct parent DOM element (depending on option orientation)
12695 var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
12696 var parentChanged = (foreground.parentNode !== parent);
12697
12698 // calculate character width and height
12699 this._calculateCharSize();
12700
12701 // TODO: recalculate sizes only needed when parent is resized or options is changed
12702 var orientation = this.options.orientation,
12703 showMinorLabels = this.options.showMinorLabels,
12704 showMajorLabels = this.options.showMajorLabels;
12705
12706 // determine the width and height of the elemens for the axis
12707 props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
12708 props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
12709 props.height = props.minorLabelHeight + props.majorLabelHeight;
12710 props.width = foreground.offsetWidth;
12711
12712 props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
12713 (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
12714 props.minorLineWidth = 1; // TODO: really calculate width
12715 props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
12716 props.majorLineWidth = 1; // TODO: really calculate width
12717
12718 // take foreground and background offline while updating (is almost twice as fast)
12719 var foregroundNextSibling = foreground.nextSibling;
12720 var backgroundNextSibling = background.nextSibling;
12721 foreground.parentNode && foreground.parentNode.removeChild(foreground);
12722 background.parentNode && background.parentNode.removeChild(background);
12723
12724 foreground.style.height = this.props.height + 'px';
12725
12726 this._repaintLabels();
12727
12728 // put DOM online again (at the same place)
12729 if (foregroundNextSibling) {
12730 parent.insertBefore(foreground, foregroundNextSibling);
12731 }
12732 else {
12733 parent.appendChild(foreground)
12734 }
12735 if (backgroundNextSibling) {
12736 this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
12737 }
12738 else {
12739 this.body.dom.backgroundVertical.appendChild(background)
12740 }
12741
12742 return this._isResized() || parentChanged;
12743 };
12744
12745 /**
12746 * Repaint major and minor text labels and vertical grid lines
12747 * @private
12748 */
12749 TimeAxis.prototype._repaintLabels = function () {
12750 var orientation = this.options.orientation;
12751
12752 // calculate range and step (step such that we have space for 7 characters per label)
12753 var start = util.convert(this.body.range.start, 'Number'),
12754 end = util.convert(this.body.range.end, 'Number'),
12755 minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
12756 -this.body.util.toTime(0).valueOf();
12757 var step = new TimeStep(new Date(start), new Date(end), minimumStep);
12758 this.step = step;
12759
12760 // Move all DOM elements to a "redundant" list, where they
12761 // can be picked for re-use, and clear the lists with lines and texts.
12762 // At the end of the function _repaintLabels, left over elements will be cleaned up
12763 var dom = this.dom;
12764 dom.redundant.majorLines = dom.majorLines;
12765 dom.redundant.majorTexts = dom.majorTexts;
12766 dom.redundant.minorLines = dom.minorLines;
12767 dom.redundant.minorTexts = dom.minorTexts;
12768 dom.majorLines = [];
12769 dom.majorTexts = [];
12770 dom.minorLines = [];
12771 dom.minorTexts = [];
12772
12773 step.first();
12774 var xFirstMajorLabel = undefined;
12775 var max = 0;
12776 while (step.hasNext() && max < 1000) {
12777 max++;
12778 var cur = step.getCurrent(),
12779 x = this.body.util.toScreen(cur),
12780 isMajor = step.isMajor();
12781
12782 // TODO: lines must have a width, such that we can create css backgrounds
12783
12784 if (this.options.showMinorLabels) {
12785 this._repaintMinorText(x, step.getLabelMinor(), orientation);
12786 }
12787
12788 if (isMajor && this.options.showMajorLabels) {
12789 if (x > 0) {
12790 if (xFirstMajorLabel == undefined) {
12791 xFirstMajorLabel = x;
12792 }
12793 this._repaintMajorText(x, step.getLabelMajor(), orientation);
12794 }
12795 this._repaintMajorLine(x, orientation);
12796 }
12797 else {
12798 this._repaintMinorLine(x, orientation);
12799 }
12800
12801 step.next();
12802 }
12803
12804 // create a major label on the left when needed
12805 if (this.options.showMajorLabels) {
12806 var leftTime = this.body.util.toTime(0),
12807 leftText = step.getLabelMajor(leftTime),
12808 widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
12809
12810 if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
12811 this._repaintMajorText(0, leftText, orientation);
12812 }
12813 }
12814
12815 // Cleanup leftover DOM elements from the redundant list
12816 util.forEach(this.dom.redundant, function (arr) {
12817 while (arr.length) {
12818 var elem = arr.pop();
12819 if (elem && elem.parentNode) {
12820 elem.parentNode.removeChild(elem);
12821 }
12822 }
12823 });
12824 };
12825
12826 /**
12827 * Create a minor label for the axis at position x
12828 * @param {Number} x
12829 * @param {String} text
12830 * @param {String} orientation "top" or "bottom" (default)
12831 * @private
12832 */
12833 TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
12834 // reuse redundant label
12835 var label = this.dom.redundant.minorTexts.shift();
12836
12837 if (!label) {
12838 // create new label
12839 var content = document.createTextNode('');
12840 label = document.createElement('div');
12841 label.appendChild(content);
12842 label.className = 'text minor';
12843 this.dom.foreground.appendChild(label);
12844 }
12845 this.dom.minorTexts.push(label);
12846
12847 label.childNodes[0].nodeValue = text;
12848
12849 label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
12850 label.style.left = x + 'px';
12851 //label.title = title; // TODO: this is a heavy operation
12852 };
12853
12854 /**
12855 * Create a Major label for the axis at position x
12856 * @param {Number} x
12857 * @param {String} text
12858 * @param {String} orientation "top" or "bottom" (default)
12859 * @private
12860 */
12861 TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
12862 // reuse redundant label
12863 var label = this.dom.redundant.majorTexts.shift();
12864
12865 if (!label) {
12866 // create label
12867 var content = document.createTextNode(text);
12868 label = document.createElement('div');
12869 label.className = 'text major';
12870 label.appendChild(content);
12871 this.dom.foreground.appendChild(label);
12872 }
12873 this.dom.majorTexts.push(label);
12874
12875 label.childNodes[0].nodeValue = text;
12876 //label.title = title; // TODO: this is a heavy operation
12877
12878 label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
12879 label.style.left = x + 'px';
12880 };
12881
12882 /**
12883 * Create a minor line for the axis at position x
12884 * @param {Number} x
12885 * @param {String} orientation "top" or "bottom" (default)
12886 * @private
12887 */
12888 TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
12889 // reuse redundant line
12890 var line = this.dom.redundant.minorLines.shift();
12891
12892 if (!line) {
12893 // create vertical line
12894 line = document.createElement('div');
12895 line.className = 'grid vertical minor';
12896 this.dom.background.appendChild(line);
12897 }
12898 this.dom.minorLines.push(line);
12899
12900 var props = this.props;
12901 if (orientation == 'top') {
12902 line.style.top = props.majorLabelHeight + 'px';
12903 }
12904 else {
12905 line.style.top = this.body.domProps.top.height + 'px';
12906 }
12907 line.style.height = props.minorLineHeight + 'px';
12908 line.style.left = (x - props.minorLineWidth / 2) + 'px';
12909 };
12910
12911 /**
12912 * Create a Major line for the axis at position x
12913 * @param {Number} x
12914 * @param {String} orientation "top" or "bottom" (default)
12915 * @private
12916 */
12917 TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
12918 // reuse redundant line
12919 var line = this.dom.redundant.majorLines.shift();
12920
12921 if (!line) {
12922 // create vertical line
12923 line = document.createElement('DIV');
12924 line.className = 'grid vertical major';
12925 this.dom.background.appendChild(line);
12926 }
12927 this.dom.majorLines.push(line);
12928
12929 var props = this.props;
12930 if (orientation == 'top') {
12931 line.style.top = '0';
12932 }
12933 else {
12934 line.style.top = this.body.domProps.top.height + 'px';
12935 }
12936 line.style.left = (x - props.majorLineWidth / 2) + 'px';
12937 line.style.height = props.majorLineHeight + 'px';
12938 };
12939
12940 /**
12941 * Determine the size of text on the axis (both major and minor axis).
12942 * The size is calculated only once and then cached in this.props.
12943 * @private
12944 */
12945 TimeAxis.prototype._calculateCharSize = function () {
12946 // Note: We calculate char size with every redraw. Size may change, for
12947 // example when any of the timelines parents had display:none for example.
12948
12949 // determine the char width and height on the minor axis
12950 if (!this.dom.measureCharMinor) {
12951 this.dom.measureCharMinor = document.createElement('DIV');
12952 this.dom.measureCharMinor.className = 'text minor measure';
12953 this.dom.measureCharMinor.style.position = 'absolute';
12954
12955 this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
12956 this.dom.foreground.appendChild(this.dom.measureCharMinor);
12957 }
12958 this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
12959 this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
12960
12961 // determine the char width and height on the major axis
12962 if (!this.dom.measureCharMajor) {
12963 this.dom.measureCharMajor = document.createElement('DIV');
12964 this.dom.measureCharMajor.className = 'text minor measure';
12965 this.dom.measureCharMajor.style.position = 'absolute';
12966
12967 this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
12968 this.dom.foreground.appendChild(this.dom.measureCharMajor);
12969 }
12970 this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
12971 this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
12972 };
12973
12974 /**
12975 * Snap a date to a rounded value.
12976 * The snap intervals are dependent on the current scale and step.
12977 * @param {Date} date the date to be snapped.
12978 * @return {Date} snappedDate
12979 */
12980 TimeAxis.prototype.snap = function(date) {
12981 return this.step.snap(date);
12982 };
12983
12984 module.exports = TimeAxis;
12985
12986
12987/***/ },
12988/* 28 */
12989/***/ function(module, exports, __webpack_require__) {
12990
12991 var Hammer = __webpack_require__(42);
12992
12993 /**
12994 * @constructor Item
12995 * @param {Object} data Object containing (optional) parameters type,
12996 * start, end, content, group, className.
12997 * @param {{toScreen: function, toTime: function}} conversion
12998 * Conversion functions from time to screen and vice versa
12999 * @param {Object} options Configuration options
13000 * // TODO: describe available options
13001 */
13002 function Item (data, conversion, options) {
13003 this.id = null;
13004 this.parent = null;
13005 this.data = data;
13006 this.dom = null;
13007 this.conversion = conversion || {};
13008 this.options = options || {};
13009
13010 this.selected = false;
13011 this.displayed = false;
13012 this.dirty = true;
13013
13014 this.top = null;
13015 this.left = null;
13016 this.width = null;
13017 this.height = null;
13018 }
13019
13020 /**
13021 * Select current item
13022 */
13023 Item.prototype.select = function() {
13024 this.selected = true;
13025 this.dirty = true;
13026 if (this.displayed) this.redraw();
13027 };
13028
13029 /**
13030 * Unselect current item
13031 */
13032 Item.prototype.unselect = function() {
13033 this.selected = false;
13034 this.dirty = true;
13035 if (this.displayed) this.redraw();
13036 };
13037
13038 /**
13039 * Set data for the item. Existing data will be updated. The id should not
13040 * be changed. When the item is displayed, it will be redrawn immediately.
13041 * @param {Object} data
13042 */
13043 Item.prototype.setData = function(data) {
13044 this.data = data;
13045 this.dirty = true;
13046 if (this.displayed) this.redraw();
13047 };
13048
13049 /**
13050 * Set a parent for the item
13051 * @param {ItemSet | Group} parent
13052 */
13053 Item.prototype.setParent = function(parent) {
13054 if (this.displayed) {
13055 this.hide();
13056 this.parent = parent;
13057 if (this.parent) {
13058 this.show();
13059 }
13060 }
13061 else {
13062 this.parent = parent;
13063 }
13064 };
13065
13066 /**
13067 * Check whether this item is visible inside given range
13068 * @returns {{start: Number, end: Number}} range with a timestamp for start and end
13069 * @returns {boolean} True if visible
13070 */
13071 Item.prototype.isVisible = function(range) {
13072 // Should be implemented by Item implementations
13073 return false;
13074 };
13075
13076 /**
13077 * Show the Item in the DOM (when not already visible)
13078 * @return {Boolean} changed
13079 */
13080 Item.prototype.show = function() {
13081 return false;
13082 };
13083
13084 /**
13085 * Hide the Item from the DOM (when visible)
13086 * @return {Boolean} changed
13087 */
13088 Item.prototype.hide = function() {
13089 return false;
13090 };
13091
13092 /**
13093 * Repaint the item
13094 */
13095 Item.prototype.redraw = function() {
13096 // should be implemented by the item
13097 };
13098
13099 /**
13100 * Reposition the Item horizontally
13101 */
13102 Item.prototype.repositionX = function() {
13103 // should be implemented by the item
13104 };
13105
13106 /**
13107 * Reposition the Item vertically
13108 */
13109 Item.prototype.repositionY = function() {
13110 // should be implemented by the item
13111 };
13112
13113 /**
13114 * Repaint a delete button on the top right of the item when the item is selected
13115 * @param {HTMLElement} anchor
13116 * @protected
13117 */
13118 Item.prototype._repaintDeleteButton = function (anchor) {
13119 if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
13120 // create and show button
13121 var me = this;
13122
13123 var deleteButton = document.createElement('div');
13124 deleteButton.className = 'delete';
13125 deleteButton.title = 'Delete this item';
13126
13127 Hammer(deleteButton, {
13128 preventDefault: true
13129 }).on('tap', function (event) {
13130 me.parent.removeFromDataSet(me);
13131 event.stopPropagation();
13132 });
13133
13134 anchor.appendChild(deleteButton);
13135 this.dom.deleteButton = deleteButton;
13136 }
13137 else if (!this.selected && this.dom.deleteButton) {
13138 // remove button
13139 if (this.dom.deleteButton.parentNode) {
13140 this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
13141 }
13142 this.dom.deleteButton = null;
13143 }
13144 };
13145
13146 /**
13147 * Set HTML contents for the item
13148 * @param {Element} element HTML element to fill with the contents
13149 * @private
13150 */
13151 Item.prototype._updateContents = function (element) {
13152 var content;
13153 if (this.options.template) {
13154 var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset
13155 content = this.options.template(itemData);
13156 }
13157 else {
13158 content = this.data.content;
13159 }
13160
13161 if (content instanceof Element) {
13162 element.innerHTML = '';
13163 element.appendChild(content);
13164 }
13165 else if (content != undefined) {
13166 element.innerHTML = content;
13167 }
13168 else {
13169 throw new Error('Property "content" missing in item ' + this.data.id);
13170 }
13171 };
13172
13173 /**
13174 * Set HTML contents for the item
13175 * @param {Element} element HTML element to fill with the contents
13176 * @private
13177 */
13178 Item.prototype._updateTitle = function (element) {
13179 if (this.data.title != null) {
13180 element.title = this.data.title || '';
13181 }
13182 else {
13183 element.removeAttribute('title');
13184 }
13185 };
13186
13187 /**
13188 * Process dataAttributes timeline option and set as data- attributes on dom.content
13189 * @param {Element} element HTML element to which the attributes will be attached
13190 * @private
13191 */
13192 Item.prototype._updateDataAttributes = function(element) {
13193 if (this.options.dataAttributes && this.options.dataAttributes.length > 0) {
13194 for (var i = 0; i < this.options.dataAttributes.length; i++) {
13195 var name = this.options.dataAttributes[i];
13196 var value = this.data[name];
13197
13198 if (value != null) {
13199 element.setAttribute('data-' + name, value);
13200 }
13201 else {
13202 element.removeAttribute('data-' + name);
13203 }
13204 }
13205 }
13206 };
13207
13208 module.exports = Item;
13209
13210
13211/***/ },
13212/* 29 */
13213/***/ function(module, exports, __webpack_require__) {
13214
13215 var Hammer = __webpack_require__(42);
13216 var Item = __webpack_require__(28);
13217 var RangeItem = __webpack_require__(32);
13218
13219 /**
13220 * @constructor BackgroundItem
13221 * @extends Item
13222 * @param {Object} data Object containing parameters start, end
13223 * content, className.
13224 * @param {{toScreen: function, toTime: function}} conversion
13225 * Conversion functions from time to screen and vice versa
13226 * @param {Object} [options] Configuration options
13227 * // TODO: describe options
13228 */
13229 // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation
13230 function BackgroundItem (data, conversion, options) {
13231 this.props = {
13232 content: {
13233 width: 0
13234 }
13235 };
13236 this.overflow = false; // if contents can overflow (css styling), this flag is set to true
13237
13238 // validate data
13239 if (data) {
13240 if (data.start == undefined) {
13241 throw new Error('Property "start" missing in item ' + data.id);
13242 }
13243 if (data.end == undefined) {
13244 throw new Error('Property "end" missing in item ' + data.id);
13245 }
13246 }
13247
13248 Item.call(this, data, conversion, options);
13249 }
13250
13251 BackgroundItem.prototype = new Item (null, null, null);
13252
13253 BackgroundItem.prototype.baseClassName = 'item background';
13254
13255 /**
13256 * Check whether this item is visible inside given range
13257 * @returns {{start: Number, end: Number}} range with a timestamp for start and end
13258 * @returns {boolean} True if visible
13259 */
13260 BackgroundItem.prototype.isVisible = function(range) {
13261 // determine visibility
13262 return (this.data.start < range.end) && (this.data.end > range.start);
13263 };
13264
13265 /**
13266 * Repaint the item
13267 */
13268 BackgroundItem.prototype.redraw = function() {
13269 var dom = this.dom;
13270 if (!dom) {
13271 // create DOM
13272 this.dom = {};
13273 dom = this.dom;
13274
13275 // background box
13276 dom.box = document.createElement('div');
13277 // className is updated in redraw()
13278
13279 // contents box
13280 dom.content = document.createElement('div');
13281 dom.content.className = 'content';
13282 dom.box.appendChild(dom.content);
13283
13284 // attach this item as attribute
13285 dom.box['timeline-item'] = this;
13286
13287 this.dirty = true;
13288 }
13289
13290 // append DOM to parent DOM
13291 if (!this.parent) {
13292 throw new Error('Cannot redraw item: no parent attached');
13293 }
13294 if (!dom.box.parentNode) {
13295 var background = this.parent.dom.background;
13296 if (!background) {
13297 throw new Error('Cannot redraw time axis: parent has no background container element');
13298 }
13299 background.appendChild(dom.box);
13300 }
13301 this.displayed = true;
13302
13303 // Update DOM when item is marked dirty. An item is marked dirty when:
13304 // - the item is not yet rendered
13305 // - the item's data is changed
13306 // - the item is selected/deselected
13307 if (this.dirty) {
13308 this._updateContents(this.dom.content);
13309 this._updateTitle(this.dom.content);
13310 this._updateDataAttributes(this.dom.content);
13311
13312 // update class
13313 var className = (this.data.className ? (' ' + this.data.className) : '') +
13314 (this.selected ? ' selected' : '');
13315 dom.box.className = this.baseClassName + className;
13316
13317 // determine from css whether this box has overflow
13318 this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
13319
13320 // recalculate size
13321 this.props.content.width = this.dom.content.offsetWidth;
13322 this.height = 0; // set height zero, so this item will be ignored when stacking items
13323
13324 this.dirty = false;
13325 }
13326 };
13327
13328 /**
13329 * Show the item in the DOM (when not already visible). The items DOM will
13330 * be created when needed.
13331 */
13332 BackgroundItem.prototype.show = RangeItem.prototype.show;
13333
13334 /**
13335 * Hide the item from the DOM (when visible)
13336 * @return {Boolean} changed
13337 */
13338 BackgroundItem.prototype.hide = RangeItem.prototype.hide;
13339
13340 /**
13341 * Reposition the item horizontally
13342 * @Override
13343 */
13344 BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX;
13345
13346 /**
13347 * Reposition the item vertically
13348 * @Override
13349 */
13350 BackgroundItem.prototype.repositionY = function() {
13351 var onTop = this.options.orientation === 'top';
13352 this.dom.content.style.top = onTop ? '' : '0';
13353 this.dom.content.style.bottom = onTop ? '0' : '';
13354 };
13355
13356 module.exports = BackgroundItem;
13357
13358
13359/***/ },
13360/* 30 */
13361/***/ function(module, exports, __webpack_require__) {
13362
13363 var Item = __webpack_require__(28);
13364
13365 /**
13366 * @constructor BoxItem
13367 * @extends Item
13368 * @param {Object} data Object containing parameters start
13369 * content, className.
13370 * @param {{toScreen: function, toTime: function}} conversion
13371 * Conversion functions from time to screen and vice versa
13372 * @param {Object} [options] Configuration options
13373 * // TODO: describe available options
13374 */
13375 function BoxItem (data, conversion, options) {
13376 this.props = {
13377 dot: {
13378 width: 0,
13379 height: 0
13380 },
13381 line: {
13382 width: 0,
13383 height: 0
13384 }
13385 };
13386
13387 // validate data
13388 if (data) {
13389 if (data.start == undefined) {
13390 throw new Error('Property "start" missing in item ' + data);
13391 }
13392 }
13393
13394 Item.call(this, data, conversion, options);
13395 }
13396
13397 BoxItem.prototype = new Item (null, null, null);
13398
13399 /**
13400 * Check whether this item is visible inside given range
13401 * @returns {{start: Number, end: Number}} range with a timestamp for start and end
13402 * @returns {boolean} True if visible
13403 */
13404 BoxItem.prototype.isVisible = function(range) {
13405 // determine visibility
13406 // TODO: account for the real width of the item. Right now we just add 1/4 to the window
13407 var interval = (range.end - range.start) / 4;
13408 return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
13409 };
13410
13411 /**
13412 * Repaint the item
13413 */
13414 BoxItem.prototype.redraw = function() {
13415 var dom = this.dom;
13416 if (!dom) {
13417 // create DOM
13418 this.dom = {};
13419 dom = this.dom;
13420
13421 // create main box
13422 dom.box = document.createElement('DIV');
13423
13424 // contents box (inside the background box). used for making margins
13425 dom.content = document.createElement('DIV');
13426 dom.content.className = 'content';
13427 dom.box.appendChild(dom.content);
13428
13429 // line to axis
13430 dom.line = document.createElement('DIV');
13431 dom.line.className = 'line';
13432
13433 // dot on axis
13434 dom.dot = document.createElement('DIV');
13435 dom.dot.className = 'dot';
13436
13437 // attach this item as attribute
13438 dom.box['timeline-item'] = this;
13439
13440 this.dirty = true;
13441 }
13442
13443 // append DOM to parent DOM
13444 if (!this.parent) {
13445 throw new Error('Cannot redraw item: no parent attached');
13446 }
13447 if (!dom.box.parentNode) {
13448 var foreground = this.parent.dom.foreground;
13449 if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
13450 foreground.appendChild(dom.box);
13451 }
13452 if (!dom.line.parentNode) {
13453 var background = this.parent.dom.background;
13454 if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
13455 background.appendChild(dom.line);
13456 }
13457 if (!dom.dot.parentNode) {
13458 var axis = this.parent.dom.axis;
13459 if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
13460 axis.appendChild(dom.dot);
13461 }
13462 this.displayed = true;
13463
13464 // Update DOM when item is marked dirty. An item is marked dirty when:
13465 // - the item is not yet rendered
13466 // - the item's data is changed
13467 // - the item is selected/deselected
13468 if (this.dirty) {
13469 this._updateContents(this.dom.content);
13470 this._updateTitle(this.dom.box);
13471 this._updateDataAttributes(this.dom.box);
13472
13473 // update class
13474 var className = (this.data.className? ' ' + this.data.className : '') +
13475 (this.selected ? ' selected' : '');
13476 dom.box.className = 'item box' + className;
13477 dom.line.className = 'item line' + className;
13478 dom.dot.className = 'item dot' + className;
13479
13480 // recalculate size
13481 this.props.dot.height = dom.dot.offsetHeight;
13482 this.props.dot.width = dom.dot.offsetWidth;
13483 this.props.line.width = dom.line.offsetWidth;
13484 this.width = dom.box.offsetWidth;
13485 this.height = dom.box.offsetHeight;
13486
13487 this.dirty = false;
13488 }
13489
13490 this._repaintDeleteButton(dom.box);
13491 };
13492
13493 /**
13494 * Show the item in the DOM (when not already displayed). The items DOM will
13495 * be created when needed.
13496 */
13497 BoxItem.prototype.show = function() {
13498 if (!this.displayed) {
13499 this.redraw();
13500 }
13501 };
13502
13503 /**
13504 * Hide the item from the DOM (when visible)
13505 */
13506 BoxItem.prototype.hide = function() {
13507 if (this.displayed) {
13508 var dom = this.dom;
13509
13510 if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
13511 if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
13512 if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
13513
13514 this.top = null;
13515 this.left = null;
13516
13517 this.displayed = false;
13518 }
13519 };
13520
13521 /**
13522 * Reposition the item horizontally
13523 * @Override
13524 */
13525 BoxItem.prototype.repositionX = function() {
13526 var start = this.conversion.toScreen(this.data.start);
13527 var align = this.options.align;
13528 var left;
13529 var box = this.dom.box;
13530 var line = this.dom.line;
13531 var dot = this.dom.dot;
13532
13533 // calculate left position of the box
13534 if (align == 'right') {
13535 this.left = start - this.width;
13536 }
13537 else if (align == 'left') {
13538 this.left = start;
13539 }
13540 else {
13541 // default or 'center'
13542 this.left = start - this.width / 2;
13543 }
13544
13545 // reposition box
13546 box.style.left = this.left + 'px';
13547
13548 // reposition line
13549 line.style.left = (start - this.props.line.width / 2) + 'px';
13550
13551 // reposition dot
13552 dot.style.left = (start - this.props.dot.width / 2) + 'px';
13553 };
13554
13555 /**
13556 * Reposition the item vertically
13557 * @Override
13558 */
13559 BoxItem.prototype.repositionY = function() {
13560 var orientation = this.options.orientation;
13561 var box = this.dom.box;
13562 var line = this.dom.line;
13563 var dot = this.dom.dot;
13564
13565 if (orientation == 'top') {
13566 box.style.top = (this.top || 0) + 'px';
13567
13568 line.style.top = '0';
13569 line.style.height = (this.parent.top + this.top + 1) + 'px';
13570 line.style.bottom = '';
13571 }
13572 else { // orientation 'bottom'
13573 var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
13574 var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
13575
13576 box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
13577 line.style.top = (itemSetHeight - lineHeight) + 'px';
13578 line.style.bottom = '0';
13579 }
13580
13581 dot.style.top = (-this.props.dot.height / 2) + 'px';
13582 };
13583
13584 module.exports = BoxItem;
13585
13586
13587/***/ },
13588/* 31 */
13589/***/ function(module, exports, __webpack_require__) {
13590
13591 var Item = __webpack_require__(28);
13592
13593 /**
13594 * @constructor PointItem
13595 * @extends Item
13596 * @param {Object} data Object containing parameters start
13597 * content, className.
13598 * @param {{toScreen: function, toTime: function}} conversion
13599 * Conversion functions from time to screen and vice versa
13600 * @param {Object} [options] Configuration options
13601 * // TODO: describe available options
13602 */
13603 function PointItem (data, conversion, options) {
13604 this.props = {
13605 dot: {
13606 top: 0,
13607 width: 0,
13608 height: 0
13609 },
13610 content: {
13611 height: 0,
13612 marginLeft: 0
13613 }
13614 };
13615
13616 // validate data
13617 if (data) {
13618 if (data.start == undefined) {
13619 throw new Error('Property "start" missing in item ' + data);
13620 }
13621 }
13622
13623 Item.call(this, data, conversion, options);
13624 }
13625
13626 PointItem.prototype = new Item (null, null, null);
13627
13628 /**
13629 * Check whether this item is visible inside given range
13630 * @returns {{start: Number, end: Number}} range with a timestamp for start and end
13631 * @returns {boolean} True if visible
13632 */
13633 PointItem.prototype.isVisible = function(range) {
13634 // determine visibility
13635 // TODO: account for the real width of the item. Right now we just add 1/4 to the window
13636 var interval = (range.end - range.start) / 4;
13637 return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
13638 };
13639
13640 /**
13641 * Repaint the item
13642 */
13643 PointItem.prototype.redraw = function() {
13644 var dom = this.dom;
13645 if (!dom) {
13646 // create DOM
13647 this.dom = {};
13648 dom = this.dom;
13649
13650 // background box
13651 dom.point = document.createElement('div');
13652 // className is updated in redraw()
13653
13654 // contents box, right from the dot
13655 dom.content = document.createElement('div');
13656 dom.content.className = 'content';
13657 dom.point.appendChild(dom.content);
13658
13659 // dot at start
13660 dom.dot = document.createElement('div');
13661 dom.point.appendChild(dom.dot);
13662
13663 // attach this item as attribute
13664 dom.point['timeline-item'] = this;
13665
13666 this.dirty = true;
13667 }
13668
13669 // append DOM to parent DOM
13670 if (!this.parent) {
13671 throw new Error('Cannot redraw item: no parent attached');
13672 }
13673 if (!dom.point.parentNode) {
13674 var foreground = this.parent.dom.foreground;
13675 if (!foreground) {
13676 throw new Error('Cannot redraw time axis: parent has no foreground container element');
13677 }
13678 foreground.appendChild(dom.point);
13679 }
13680 this.displayed = true;
13681
13682 // Update DOM when item is marked dirty. An item is marked dirty when:
13683 // - the item is not yet rendered
13684 // - the item's data is changed
13685 // - the item is selected/deselected
13686 if (this.dirty) {
13687 this._updateContents(this.dom.content);
13688 this._updateTitle(this.dom.point);
13689 this._updateDataAttributes(this.dom.point);
13690
13691 // update class
13692 var className = (this.data.className? ' ' + this.data.className : '') +
13693 (this.selected ? ' selected' : '');
13694 dom.point.className = 'item point' + className;
13695 dom.dot.className = 'item dot' + className;
13696
13697 // recalculate size
13698 this.width = dom.point.offsetWidth;
13699 this.height = dom.point.offsetHeight;
13700 this.props.dot.width = dom.dot.offsetWidth;
13701 this.props.dot.height = dom.dot.offsetHeight;
13702 this.props.content.height = dom.content.offsetHeight;
13703
13704 // resize contents
13705 dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
13706 //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
13707
13708 dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
13709 dom.dot.style.left = (this.props.dot.width / 2) + 'px';
13710
13711 this.dirty = false;
13712 }
13713
13714 this._repaintDeleteButton(dom.point);
13715 };
13716
13717 /**
13718 * Show the item in the DOM (when not already visible). The items DOM will
13719 * be created when needed.
13720 */
13721 PointItem.prototype.show = function() {
13722 if (!this.displayed) {
13723 this.redraw();
13724 }
13725 };
13726
13727 /**
13728 * Hide the item from the DOM (when visible)
13729 */
13730 PointItem.prototype.hide = function() {
13731 if (this.displayed) {
13732 if (this.dom.point.parentNode) {
13733 this.dom.point.parentNode.removeChild(this.dom.point);
13734 }
13735
13736 this.top = null;
13737 this.left = null;
13738
13739 this.displayed = false;
13740 }
13741 };
13742
13743 /**
13744 * Reposition the item horizontally
13745 * @Override
13746 */
13747 PointItem.prototype.repositionX = function() {
13748 var start = this.conversion.toScreen(this.data.start);
13749
13750 this.left = start - this.props.dot.width;
13751
13752 // reposition point
13753 this.dom.point.style.left = this.left + 'px';
13754 };
13755
13756 /**
13757 * Reposition the item vertically
13758 * @Override
13759 */
13760 PointItem.prototype.repositionY = function() {
13761 var orientation = this.options.orientation,
13762 point = this.dom.point;
13763
13764 if (orientation == 'top') {
13765 point.style.top = this.top + 'px';
13766 }
13767 else {
13768 point.style.top = (this.parent.height - this.top - this.height) + 'px';
13769 }
13770 };
13771
13772 module.exports = PointItem;
13773
13774
13775/***/ },
13776/* 32 */
13777/***/ function(module, exports, __webpack_require__) {
13778
13779 var Hammer = __webpack_require__(42);
13780 var Item = __webpack_require__(28);
13781
13782 /**
13783 * @constructor RangeItem
13784 * @extends Item
13785 * @param {Object} data Object containing parameters start, end
13786 * content, className.
13787 * @param {{toScreen: function, toTime: function}} conversion
13788 * Conversion functions from time to screen and vice versa
13789 * @param {Object} [options] Configuration options
13790 * // TODO: describe options
13791 */
13792 function RangeItem (data, conversion, options) {
13793 this.props = {
13794 content: {
13795 width: 0
13796 }
13797 };
13798 this.overflow = false; // if contents can overflow (css styling), this flag is set to true
13799
13800 // validate data
13801 if (data) {
13802 if (data.start == undefined) {
13803 throw new Error('Property "start" missing in item ' + data.id);
13804 }
13805 if (data.end == undefined) {
13806 throw new Error('Property "end" missing in item ' + data.id);
13807 }
13808 }
13809
13810 Item.call(this, data, conversion, options);
13811 }
13812
13813 RangeItem.prototype = new Item (null, null, null);
13814
13815 RangeItem.prototype.baseClassName = 'item range';
13816
13817 /**
13818 * Check whether this item is visible inside given range
13819 * @returns {{start: Number, end: Number}} range with a timestamp for start and end
13820 * @returns {boolean} True if visible
13821 */
13822 RangeItem.prototype.isVisible = function(range) {
13823 // determine visibility
13824 return (this.data.start < range.end) && (this.data.end > range.start);
13825 };
13826
13827 /**
13828 * Repaint the item
13829 */
13830 RangeItem.prototype.redraw = function() {
13831 var dom = this.dom;
13832 if (!dom) {
13833 // create DOM
13834 this.dom = {};
13835 dom = this.dom;
13836
13837 // background box
13838 dom.box = document.createElement('div');
13839 // className is updated in redraw()
13840
13841 // contents box
13842 dom.content = document.createElement('div');
13843 dom.content.className = 'content';
13844 dom.box.appendChild(dom.content);
13845
13846 // attach this item as attribute
13847 dom.box['timeline-item'] = this;
13848
13849 this.dirty = true;
13850 }
13851
13852 // append DOM to parent DOM
13853 if (!this.parent) {
13854 throw new Error('Cannot redraw item: no parent attached');
13855 }
13856 if (!dom.box.parentNode) {
13857 var foreground = this.parent.dom.foreground;
13858 if (!foreground) {
13859 throw new Error('Cannot redraw time axis: parent has no foreground container element');
13860 }
13861 foreground.appendChild(dom.box);
13862 }
13863 this.displayed = true;
13864
13865 // Update DOM when item is marked dirty. An item is marked dirty when:
13866 // - the item is not yet rendered
13867 // - the item's data is changed
13868 // - the item is selected/deselected
13869 if (this.dirty) {
13870 this._updateContents(this.dom.content);
13871 this._updateTitle(this.dom.box);
13872 this._updateDataAttributes(this.dom.box);
13873
13874 // update class
13875 var className = (this.data.className ? (' ' + this.data.className) : '') +
13876 (this.selected ? ' selected' : '');
13877 dom.box.className = this.baseClassName + className;
13878
13879 // determine from css whether this box has overflow
13880 this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
13881
13882 // recalculate size
13883 this.props.content.width = this.dom.content.offsetWidth;
13884 this.height = this.dom.box.offsetHeight;
13885
13886 this.dirty = false;
13887 }
13888
13889 this._repaintDeleteButton(dom.box);
13890 this._repaintDragLeft();
13891 this._repaintDragRight();
13892 };
13893
13894 /**
13895 * Show the item in the DOM (when not already visible). The items DOM will
13896 * be created when needed.
13897 */
13898 RangeItem.prototype.show = function() {
13899 if (!this.displayed) {
13900 this.redraw();
13901 }
13902 };
13903
13904 /**
13905 * Hide the item from the DOM (when visible)
13906 * @return {Boolean} changed
13907 */
13908 RangeItem.prototype.hide = function() {
13909 if (this.displayed) {
13910 var box = this.dom.box;
13911
13912 if (box.parentNode) {
13913 box.parentNode.removeChild(box);
13914 }
13915
13916 this.top = null;
13917 this.left = null;
13918
13919 this.displayed = false;
13920 }
13921 };
13922
13923 /**
13924 * Reposition the item horizontally
13925 * @Override
13926 */
13927 RangeItem.prototype.repositionX = function() {
13928 var parentWidth = this.parent.width;
13929 var start = this.conversion.toScreen(this.data.start);
13930 var end = this.conversion.toScreen(this.data.end);
13931 var contentLeft;
13932 var contentWidth;
13933
13934 // limit the width of the this, as browsers cannot draw very wide divs
13935 if (start < -parentWidth) {
13936 start = -parentWidth;
13937 }
13938 if (end > 2 * parentWidth) {
13939 end = 2 * parentWidth;
13940 }
13941 var boxWidth = Math.max(end - start, 1);
13942
13943 if (this.overflow) {
13944 this.left = start;
13945 this.width = boxWidth + this.props.content.width;
13946 contentWidth = this.props.content.width;
13947
13948 // Note: The calculation of width is an optimistic calculation, giving
13949 // a width which will not change when moving the Timeline
13950 // So no re-stacking needed, which is nicer for the eye;
13951 }
13952 else {
13953 this.left = start;
13954 this.width = boxWidth;
13955 contentWidth = Math.min(end - start, this.props.content.width);
13956 }
13957
13958 this.dom.box.style.left = this.left + 'px';
13959 this.dom.box.style.width = boxWidth + 'px';
13960
13961 switch (this.options.align) {
13962 case 'left':
13963 this.dom.content.style.left = '0';
13964 break;
13965
13966 case 'right':
13967 this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px';
13968 break;
13969
13970 case 'center':
13971 this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px';
13972 break;
13973
13974 default: // 'auto'
13975 if (this.overflow) {
13976 // when range exceeds left of the window, position the contents at the left of the visible area
13977 contentLeft = Math.max(-start, 0);
13978 }
13979 else {
13980 // when range exceeds left of the window, position the contents at the left of the visible area
13981 if (start < 0) {
13982 contentLeft = Math.min(-start,
13983 (end - start - this.props.content.width - 2 * this.options.padding));
13984 // TODO: remove the need for options.padding. it's terrible.
13985 }
13986 else {
13987 contentLeft = 0;
13988 }
13989 }
13990 this.dom.content.style.left = contentLeft + 'px';
13991 }
13992 };
13993
13994 /**
13995 * Reposition the item vertically
13996 * @Override
13997 */
13998 RangeItem.prototype.repositionY = function() {
13999 var orientation = this.options.orientation,
14000 box = this.dom.box;
14001
14002 if (orientation == 'top') {
14003 box.style.top = this.top + 'px';
14004 }
14005 else {
14006 box.style.top = (this.parent.height - this.top - this.height) + 'px';
14007 }
14008 };
14009
14010 /**
14011 * Repaint a drag area on the left side of the range when the range is selected
14012 * @protected
14013 */
14014 RangeItem.prototype._repaintDragLeft = function () {
14015 if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
14016 // create and show drag area
14017 var dragLeft = document.createElement('div');
14018 dragLeft.className = 'drag-left';
14019 dragLeft.dragLeftItem = this;
14020
14021 // TODO: this should be redundant?
14022 Hammer(dragLeft, {
14023 preventDefault: true
14024 }).on('drag', function () {
14025 //console.log('drag left')
14026 });
14027
14028 this.dom.box.appendChild(dragLeft);
14029 this.dom.dragLeft = dragLeft;
14030 }
14031 else if (!this.selected && this.dom.dragLeft) {
14032 // delete drag area
14033 if (this.dom.dragLeft.parentNode) {
14034 this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
14035 }
14036 this.dom.dragLeft = null;
14037 }
14038 };
14039
14040 /**
14041 * Repaint a drag area on the right side of the range when the range is selected
14042 * @protected
14043 */
14044 RangeItem.prototype._repaintDragRight = function () {
14045 if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
14046 // create and show drag area
14047 var dragRight = document.createElement('div');
14048 dragRight.className = 'drag-right';
14049 dragRight.dragRightItem = this;
14050
14051 // TODO: this should be redundant?
14052 Hammer(dragRight, {
14053 preventDefault: true
14054 }).on('drag', function () {
14055 //console.log('drag right')
14056 });
14057
14058 this.dom.box.appendChild(dragRight);
14059 this.dom.dragRight = dragRight;
14060 }
14061 else if (!this.selected && this.dom.dragRight) {
14062 // delete drag area
14063 if (this.dom.dragRight.parentNode) {
14064 this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
14065 }
14066 this.dom.dragRight = null;
14067 }
14068 };
14069
14070 module.exports = RangeItem;
14071
14072
14073/***/ },
14074/* 33 */
14075/***/ function(module, exports, __webpack_require__) {
14076
14077 var Emitter = __webpack_require__(50);
14078 var Hammer = __webpack_require__(42);
14079 var mousetrap = __webpack_require__(51);
14080 var util = __webpack_require__(1);
14081 var hammerUtil = __webpack_require__(43);
14082 var DataSet = __webpack_require__(3);
14083 var DataView = __webpack_require__(4);
14084 var dotparser = __webpack_require__(39);
14085 var gephiParser = __webpack_require__(40);
14086 var Groups = __webpack_require__(35);
14087 var Images = __webpack_require__(36);
14088 var Node = __webpack_require__(37);
14089 var Edge = __webpack_require__(34);
14090 var Popup = __webpack_require__(38);
14091 var MixinLoader = __webpack_require__(48);
14092 var Activator = __webpack_require__(49);
14093 var locales = __webpack_require__(46);
14094
14095 // Load custom shapes into CanvasRenderingContext2D
14096 __webpack_require__(47);
14097
14098 /**
14099 * @constructor Network
14100 * Create a network visualization, displaying nodes and edges.
14101 *
14102 * @param {Element} container The DOM element in which the Network will
14103 * be created. Normally a div element.
14104 * @param {Object} data An object containing parameters
14105 * {Array} nodes
14106 * {Array} edges
14107 * @param {Object} options Options
14108 */
14109 function Network (container, data, options) {
14110 if (!(this instanceof Network)) {
14111 throw new SyntaxError('Constructor must be called with the new operator');
14112 }
14113
14114 this._initializeMixinLoaders();
14115
14116 // create variables and set default values
14117 this.containerElement = container;
14118
14119 // render and calculation settings
14120 this.renderRefreshRate = 60; // hz (fps)
14121 this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
14122 this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
14123 this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
14124 this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
14125
14126 this.initializing = true;
14127
14128 this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
14129
14130 // set constant values
14131 this.defaultOptions = {
14132 nodes: {
14133 mass: 1,
14134 radiusMin: 10,
14135 radiusMax: 30,
14136 radius: 10,
14137 shape: 'ellipse',
14138 image: undefined,
14139 widthMin: 16, // px
14140 widthMax: 64, // px
14141 fontColor: 'black',
14142 fontSize: 14, // px
14143 fontFace: 'verdana',
14144 fontFill: undefined,
14145 level: -1,
14146 color: {
14147 border: '#2B7CE9',
14148 background: '#97C2FC',
14149 highlight: {
14150 border: '#2B7CE9',
14151 background: '#D2E5FF'
14152 },
14153 hover: {
14154 border: '#2B7CE9',
14155 background: '#D2E5FF'
14156 }
14157 },
14158 borderColor: '#2B7CE9',
14159 backgroundColor: '#97C2FC',
14160 highlightColor: '#D2E5FF',
14161 group: undefined,
14162 borderWidth: 1,
14163 borderWidthSelected: undefined
14164 },
14165 edges: {
14166 widthMin: 1, //
14167 widthMax: 15,//
14168 width: 1,
14169 widthSelectionMultiplier: 2,
14170 hoverWidth: 1.5,
14171 style: 'line',
14172 color: {
14173 color:'#848484',
14174 highlight:'#848484',
14175 hover: '#848484'
14176 },
14177 fontColor: '#343434',
14178 fontSize: 14, // px
14179 fontFace: 'arial',
14180 fontFill: 'white',
14181 arrowScaleFactor: 1,
14182 dash: {
14183 length: 10,
14184 gap: 5,
14185 altLength: undefined
14186 },
14187 inheritColor: "from" // to, from, false, true (== from)
14188 },
14189 configurePhysics:false,
14190 physics: {
14191 barnesHut: {
14192 enabled: true,
14193 theta: 1 / 0.6, // inverted to save time during calculation
14194 gravitationalConstant: -2000,
14195 centralGravity: 0.3,
14196 springLength: 95,
14197 springConstant: 0.04,
14198 damping: 0.09
14199 },
14200 repulsion: {
14201 centralGravity: 0.0,
14202 springLength: 200,
14203 springConstant: 0.05,
14204 nodeDistance: 100,
14205 damping: 0.09
14206 },
14207 hierarchicalRepulsion: {
14208 enabled: false,
14209 centralGravity: 0.0,
14210 springLength: 100,
14211 springConstant: 0.01,
14212 nodeDistance: 150,
14213 damping: 0.09
14214 },
14215 damping: null,
14216 centralGravity: null,
14217 springLength: null,
14218 springConstant: null
14219 },
14220 clustering: { // Per Node in Cluster = PNiC
14221 enabled: false, // (Boolean) | global on/off switch for clustering.
14222 initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
14223 clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
14224 reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
14225 chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
14226 clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
14227 sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
14228 screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
14229 fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
14230 maxFontSize: 1000,
14231 forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
14232 distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
14233 edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
14234 nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
14235 height: 1, // (px PNiC) | growth of the height per node in cluster.
14236 radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
14237 maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
14238 activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
14239 clusterLevelDifference: 2
14240 },
14241 navigation: {
14242 enabled: false
14243 },
14244 keyboard: {
14245 enabled: false,
14246 speed: {x: 10, y: 10, zoom: 0.02}
14247 },
14248 dataManipulation: {
14249 enabled: false,
14250 initiallyVisible: false
14251 },
14252 hierarchicalLayout: {
14253 enabled:false,
14254 levelSeparation: 150,
14255 nodeSpacing: 100,
14256 direction: "UD", // UD, DU, LR, RL
14257 layout: "hubsize" // hubsize, directed
14258 },
14259 freezeForStabilization: false,
14260 smoothCurves: {
14261 enabled: true,
14262 dynamic: true,
14263 type: "continuous",
14264 roundness: 0.5
14265 },
14266 dynamicSmoothCurves: true,
14267 maxVelocity: 30,
14268 minVelocity: 0.1, // px/s
14269 stabilize: true, // stabilize before displaying the network
14270 stabilizationIterations: 1000, // maximum number of iteration to stabilize
14271 locale: 'en',
14272 locales: locales,
14273 tooltip: {
14274 delay: 300,
14275 fontColor: 'black',
14276 fontSize: 14, // px
14277 fontFace: 'verdana',
14278 color: {
14279 border: '#666',
14280 background: '#FFFFC6'
14281 }
14282 },
14283 dragNetwork: true,
14284 dragNodes: true,
14285 zoomable: true,
14286 hover: false,
14287 hideEdgesOnDrag: false,
14288 hideNodesOnDrag: false,
14289 width : '100%',
14290 height : '100%',
14291 selectable: true
14292 };
14293 this.constants = util.extend({}, this.defaultOptions);
14294
14295 this.hoverObj = {nodes:{},edges:{}};
14296 this.controlNodesActive = false;
14297 this.navigationHammers = {existing:[], new: []};
14298
14299 // animation properties
14300 this.animationSpeed = 1/this.renderRefreshRate;
14301 this.animationEasingFunction = "easeInOutQuint";
14302 this.easingTime = 0;
14303 this.sourceScale = 0;
14304 this.targetScale = 0;
14305 this.sourceTranslation = 0;
14306 this.targetTranslation = 0;
14307 this.lockedOnNodeId = null;
14308 this.lockedOnNodeOffset = null;
14309
14310 // Node variables
14311 var network = this;
14312 this.groups = new Groups(); // object with groups
14313 this.images = new Images(); // object with images
14314 this.images.setOnloadCallback(function () {
14315 network._redraw();
14316 });
14317
14318 // keyboard navigation variables
14319 this.xIncrement = 0;
14320 this.yIncrement = 0;
14321 this.zoomIncrement = 0;
14322
14323 // loading all the mixins:
14324 // load the force calculation functions, grouped under the physics system.
14325 this._loadPhysicsSystem();
14326 // create a frame and canvas
14327 this._create();
14328 // load the sector system. (mandatory, fully integrated with Network)
14329 this._loadSectorSystem();
14330 // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
14331 this._loadClusterSystem();
14332 // load the selection system. (mandatory, required by Network)
14333 this._loadSelectionSystem();
14334 // load the selection system. (mandatory, required by Network)
14335 this._loadHierarchySystem();
14336
14337
14338 // apply options
14339 this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
14340 this._setScale(1);
14341 this.setOptions(options);
14342
14343 // other vars
14344 this.freezeSimulation = false;// freeze the simulation
14345 this.cachedFunctions = {};
14346 this.stabilized = false;
14347 this.stabilizationIterations = null;
14348
14349 // containers for nodes and edges
14350 this.calculationNodes = {};
14351 this.calculationNodeIndices = [];
14352 this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
14353 this.nodes = {}; // object with Node objects
14354 this.edges = {}; // object with Edge objects
14355
14356 // position and scale variables and objects
14357 this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
14358 this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
14359 this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
14360 this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
14361 this.scale = 1; // defining the global scale variable in the constructor
14362 this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
14363
14364 // datasets or dataviews
14365 this.nodesData = null; // A DataSet or DataView
14366 this.edgesData = null; // A DataSet or DataView
14367
14368 // create event listeners used to subscribe on the DataSets of the nodes and edges
14369 this.nodesListeners = {
14370 'add': function (event, params) {
14371 network._addNodes(params.items);
14372 network.start();
14373 },
14374 'update': function (event, params) {
14375 network._updateNodes(params.items);
14376 network.start();
14377 },
14378 'remove': function (event, params) {
14379 network._removeNodes(params.items);
14380 network.start();
14381 }
14382 };
14383 this.edgesListeners = {
14384 'add': function (event, params) {
14385 network._addEdges(params.items);
14386 network.start();
14387 },
14388 'update': function (event, params) {
14389 network._updateEdges(params.items);
14390 network.start();
14391 },
14392 'remove': function (event, params) {
14393 network._removeEdges(params.items);
14394 network.start();
14395 }
14396 };
14397
14398 // properties for the animation
14399 this.moving = true;
14400 this.timer = undefined; // Scheduling function. Is definded in this.start();
14401
14402 // load data (the disable start variable will be the same as the enabled clustering)
14403 this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
14404
14405 // hierarchical layout
14406 this.initializing = false;
14407 if (this.constants.hierarchicalLayout.enabled == true) {
14408 this._setupHierarchicalLayout();
14409 }
14410 else {
14411 // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
14412 if (this.constants.stabilize == false) {
14413 this.zoomExtent(undefined, true,this.constants.clustering.enabled);
14414 }
14415 }
14416
14417 // if clustering is disabled, the simulation will have started in the setData function
14418 if (this.constants.clustering.enabled) {
14419 this.startWithClustering();
14420 }
14421 }
14422
14423 // Extend Network with an Emitter mixin
14424 Emitter(Network.prototype);
14425
14426 /**
14427 * Get the script path where the vis.js library is located
14428 *
14429 * @returns {string | null} path Path or null when not found. Path does not
14430 * end with a slash.
14431 * @private
14432 */
14433 Network.prototype._getScriptPath = function() {
14434 var scripts = document.getElementsByTagName( 'script' );
14435
14436 // find script named vis.js or vis.min.js
14437 for (var i = 0; i < scripts.length; i++) {
14438 var src = scripts[i].src;
14439 var match = src && /\/?vis(.min)?\.js$/.exec(src);
14440 if (match) {
14441 // return path without the script name
14442 return src.substring(0, src.length - match[0].length);
14443 }
14444 }
14445
14446 return null;
14447 };
14448
14449
14450 /**
14451 * Find the center position of the network
14452 * @private
14453 */
14454 Network.prototype._getRange = function() {
14455 var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
14456 for (var nodeId in this.nodes) {
14457 if (this.nodes.hasOwnProperty(nodeId)) {
14458 node = this.nodes[nodeId];
14459 if (minX > (node.x)) {minX = node.x;}
14460 if (maxX < (node.x)) {maxX = node.x;}
14461 if (minY > (node.y)) {minY = node.y;}
14462 if (maxY < (node.y)) {maxY = node.y;}
14463 }
14464 }
14465 if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
14466 minY = 0, maxY = 0, minX = 0, maxX = 0;
14467 }
14468 return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
14469 };
14470
14471
14472 /**
14473 * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
14474 * @returns {{x: number, y: number}}
14475 * @private
14476 */
14477 Network.prototype._findCenter = function(range) {
14478 return {x: (0.5 * (range.maxX + range.minX)),
14479 y: (0.5 * (range.maxY + range.minY))};
14480 };
14481
14482
14483 /**
14484 * This function zooms out to fit all data on screen based on amount of nodes
14485 *
14486 * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
14487 * @param {Boolean} [disableStart] | If true, start is not called.
14488 */
14489 Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableStart) {
14490 if (initialZoom === undefined) {
14491 initialZoom = false;
14492 }
14493 if (disableStart === undefined) {
14494 disableStart = false;
14495 }
14496 if (animationOptions === undefined) {
14497 animationOptions = false;
14498 }
14499
14500 var range = this._getRange();
14501 var zoomLevel;
14502
14503 if (initialZoom == true) {
14504 var numberOfNodes = this.nodeIndices.length;
14505 if (this.constants.smoothCurves == true) {
14506 if (this.constants.clustering.enabled == true &&
14507 numberOfNodes >= this.constants.clustering.initialMaxNodes) {
14508 zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
14509 }
14510 else {
14511 zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
14512 }
14513 }
14514 else {
14515 if (this.constants.clustering.enabled == true &&
14516 numberOfNodes >= this.constants.clustering.initialMaxNodes) {
14517 zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
14518 }
14519 else {
14520 zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
14521 }
14522 }
14523
14524 // correct for larger canvasses.
14525 var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
14526 zoomLevel *= factor;
14527 }
14528 else {
14529 var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
14530 var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
14531
14532 var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
14533 var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
14534
14535 zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
14536 }
14537
14538 if (zoomLevel > 1.0) {
14539 zoomLevel = 1.0;
14540 }
14541
14542
14543 var center = this._findCenter(range);
14544 if (disableStart == false) {
14545 var options = {position: center, scale: zoomLevel, animation: animationOptions};
14546 this.moveTo(options);
14547 this.moving = true;
14548 this.start();
14549 }
14550 else {
14551 center.x *= zoomLevel;
14552 center.y *= zoomLevel;
14553 center.x -= 0.5 * this.frame.canvas.clientWidth;
14554 center.y -= 0.5 * this.frame.canvas.clientHeight;
14555 this._setScale(zoomLevel);
14556 this._setTranslation(-center.x,-center.y);
14557 }
14558 };
14559
14560
14561 /**
14562 * Update the this.nodeIndices with the most recent node index list
14563 * @private
14564 */
14565 Network.prototype._updateNodeIndexList = function() {
14566 this._clearNodeIndexList();
14567 for (var idx in this.nodes) {
14568 if (this.nodes.hasOwnProperty(idx)) {
14569 this.nodeIndices.push(idx);
14570 }
14571 }
14572 };
14573
14574
14575 /**
14576 * Set nodes and edges, and optionally options as well.
14577 *
14578 * @param {Object} data Object containing parameters:
14579 * {Array | DataSet | DataView} [nodes] Array with nodes
14580 * {Array | DataSet | DataView} [edges] Array with edges
14581 * {String} [dot] String containing data in DOT format
14582 * {String} [gephi] String containing data in gephi JSON format
14583 * {Options} [options] Object with options
14584 * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
14585 */
14586 Network.prototype.setData = function(data, disableStart) {
14587 if (disableStart === undefined) {
14588 disableStart = false;
14589 }
14590 // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
14591 this.initializing = true;
14592
14593 if (data && data.dot && (data.nodes || data.edges)) {
14594 throw new SyntaxError('Data must contain either parameter "dot" or ' +
14595 ' parameter pair "nodes" and "edges", but not both.');
14596 }
14597
14598 // set options
14599 this.setOptions(data && data.options);
14600 // set all data
14601 if (data && data.dot) {
14602 // parse DOT file
14603 if(data && data.dot) {
14604 var dotData = dotparser.DOTToGraph(data.dot);
14605 this.setData(dotData);
14606 return;
14607 }
14608 }
14609 else if (data && data.gephi) {
14610 // parse DOT file
14611 if(data && data.gephi) {
14612 var gephiData = gephiParser.parseGephi(data.gephi);
14613 this.setData(gephiData);
14614 return;
14615 }
14616 }
14617 else {
14618 this._setNodes(data && data.nodes);
14619 this._setEdges(data && data.edges);
14620 }
14621 this._putDataInSector();
14622 if (disableStart == false) {
14623 if (this.constants.hierarchicalLayout.enabled == true) {
14624 this._resetLevels();
14625 this._setupHierarchicalLayout();
14626 }
14627 else {
14628 // find a stable position or start animating to a stable position
14629 if (this.constants.stabilize) {
14630 this._stabilize();
14631 }
14632 }
14633 this.start();
14634 }
14635 this.initializing = false;
14636 };
14637
14638 /**
14639 * Set options
14640 * @param {Object} options
14641 */
14642 Network.prototype.setOptions = function (options) {
14643 if (options) {
14644 var prop;
14645
14646 var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation','keyboard','dataManipulation',
14647 'onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
14648 ];
14649 util.selectiveNotDeepExtend(fields,this.constants, options);
14650 util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
14651 util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
14652
14653 if (options.physics) {
14654 util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
14655 util.mergeOptions(this.constants.physics, options.physics,'repulsion');
14656
14657 if (options.physics.hierarchicalRepulsion) {
14658 this.constants.hierarchicalLayout.enabled = true;
14659 this.constants.physics.hierarchicalRepulsion.enabled = true;
14660 this.constants.physics.barnesHut.enabled = false;
14661 for (prop in options.physics.hierarchicalRepulsion) {
14662 if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
14663 this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
14664 }
14665 }
14666 }
14667 }
14668
14669 if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
14670 if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
14671 if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
14672 if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
14673 if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
14674
14675 util.mergeOptions(this.constants, options,'smoothCurves');
14676 util.mergeOptions(this.constants, options,'hierarchicalLayout');
14677 util.mergeOptions(this.constants, options,'clustering');
14678 util.mergeOptions(this.constants, options,'navigation');
14679 util.mergeOptions(this.constants, options,'keyboard');
14680 util.mergeOptions(this.constants, options,'dataManipulation');
14681
14682
14683 if (options.dataManipulation) {
14684 this.editMode = this.constants.dataManipulation.initiallyVisible;
14685 }
14686
14687
14688 // TODO: work out these options and document them
14689 if (options.edges) {
14690 if (options.edges.color !== undefined) {
14691 if (util.isString(options.edges.color)) {
14692 this.constants.edges.color = {};
14693 this.constants.edges.color.color = options.edges.color;
14694 this.constants.edges.color.highlight = options.edges.color;
14695 this.constants.edges.color.hover = options.edges.color;
14696 }
14697 else {
14698 if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
14699 if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
14700 if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
14701 }
14702 }
14703
14704 if (!options.edges.fontColor) {
14705 if (options.edges.color !== undefined) {
14706 if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
14707 else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
14708 }
14709 }
14710 }
14711
14712 if (options.nodes) {
14713 if (options.nodes.color) {
14714 var newColorObj = util.parseColor(options.nodes.color);
14715 this.constants.nodes.color.background = newColorObj.background;
14716 this.constants.nodes.color.border = newColorObj.border;
14717 this.constants.nodes.color.highlight.background = newColorObj.highlight.background;
14718 this.constants.nodes.color.highlight.border = newColorObj.highlight.border;
14719 this.constants.nodes.color.hover.background = newColorObj.hover.background;
14720 this.constants.nodes.color.hover.border = newColorObj.hover.border;
14721 }
14722 }
14723 if (options.groups) {
14724 for (var groupname in options.groups) {
14725 if (options.groups.hasOwnProperty(groupname)) {
14726 var group = options.groups[groupname];
14727 this.groups.add(groupname, group);
14728 }
14729 }
14730 }
14731
14732 if (options.tooltip) {
14733 for (prop in options.tooltip) {
14734 if (options.tooltip.hasOwnProperty(prop)) {
14735 this.constants.tooltip[prop] = options.tooltip[prop];
14736 }
14737 }
14738 if (options.tooltip.color) {
14739 this.constants.tooltip.color = util.parseColor(options.tooltip.color);
14740 }
14741 }
14742
14743 if ('clickToUse' in options) {
14744 if (options.clickToUse) {
14745 this.activator = new Activator(this.frame);
14746 this.activator.on('change', this._createKeyBinds.bind(this));
14747 }
14748 else {
14749 if (this.activator) {
14750 this.activator.destroy();
14751 delete this.activator;
14752 }
14753 }
14754 }
14755
14756 if (options.labels) {
14757 throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
14758 }
14759 }
14760
14761 // (Re)loading the mixins that can be enabled or disabled in the options.
14762 // load the force calculation functions, grouped under the physics system.
14763 this._loadPhysicsSystem();
14764 // load the navigation system.
14765 this._loadNavigationControls();
14766 // load the data manipulation system
14767 this._loadManipulationSystem();
14768 // configure the smooth curves
14769 this._configureSmoothCurves();
14770
14771
14772 // bind keys. If disabled, this will not do anything;
14773 this._createKeyBinds();
14774 this.setSize(this.constants.width, this.constants.height);
14775 this.moving = true;
14776 this.start();
14777 };
14778
14779 /**
14780 * Create the main frame for the Network.
14781 * This function is executed once when a Network object is created. The frame
14782 * contains a canvas, and this canvas contains all objects like the axis and
14783 * nodes.
14784 * @private
14785 */
14786 Network.prototype._create = function () {
14787 // remove all elements from the container element.
14788 while (this.containerElement.hasChildNodes()) {
14789 this.containerElement.removeChild(this.containerElement.firstChild);
14790 }
14791
14792 this.frame = document.createElement('div');
14793 this.frame.className = 'vis network-frame';
14794 this.frame.style.position = 'relative';
14795 this.frame.style.overflow = 'hidden';
14796
14797 // create the network canvas (HTML canvas element)
14798 this.frame.canvas = document.createElement( 'canvas' );
14799 this.frame.canvas.style.position = 'relative';
14800 this.frame.appendChild(this.frame.canvas);
14801 if (!this.frame.canvas.getContext) {
14802 var noCanvas = document.createElement( 'DIV' );
14803 noCanvas.style.color = 'red';
14804 noCanvas.style.fontWeight = 'bold' ;
14805 noCanvas.style.padding = '10px';
14806 noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
14807 this.frame.canvas.appendChild(noCanvas);
14808 }
14809
14810 var me = this;
14811 this.drag = {};
14812 this.pinch = {};
14813 this.hammer = Hammer(this.frame.canvas, {
14814 prevent_default: true
14815 });
14816 this.hammer.on('tap', me._onTap.bind(me) );
14817 this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
14818 this.hammer.on('hold', me._onHold.bind(me) );
14819 this.hammer.on('pinch', me._onPinch.bind(me) );
14820 this.hammer.on('touch', me._onTouch.bind(me) );
14821 this.hammer.on('dragstart', me._onDragStart.bind(me) );
14822 this.hammer.on('drag', me._onDrag.bind(me) );
14823 this.hammer.on('dragend', me._onDragEnd.bind(me) );
14824 this.hammer.on('release', me._onRelease.bind(me) );
14825 this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
14826 this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
14827 this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
14828
14829 // add the frame to the container element
14830 this.containerElement.appendChild(this.frame);
14831
14832 };
14833
14834
14835 /**
14836 * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
14837 * @private
14838 */
14839 Network.prototype._createKeyBinds = function() {
14840 var me = this;
14841 this.mousetrap = mousetrap;
14842
14843 this.mousetrap.reset();
14844
14845 if (this.constants.keyboard.enabled && this.isActive()) {
14846 this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
14847 this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
14848 this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
14849 this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
14850 this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
14851 this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
14852 this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
14853 this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
14854 this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
14855 this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
14856 this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
14857 this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
14858 this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
14859 this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
14860 this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
14861 this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
14862 this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
14863 this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
14864 this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
14865 this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
14866 }
14867
14868 if (this.constants.dataManipulation.enabled == true) {
14869 this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
14870 this.mousetrap.bind("del",this._deleteSelected.bind(me));
14871 }
14872 };
14873
14874 /**
14875 * Get the pointer location from a touch location
14876 * @param {{pageX: Number, pageY: Number}} touch
14877 * @return {{x: Number, y: Number}} pointer
14878 * @private
14879 */
14880 Network.prototype._getPointer = function (touch) {
14881 return {
14882 x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas),
14883 y: touch.pageY - util.getAbsoluteTop(this.frame.canvas)
14884 };
14885 };
14886
14887 /**
14888 * On start of a touch gesture, store the pointer
14889 * @param event
14890 * @private
14891 */
14892 Network.prototype._onTouch = function (event) {
14893 this.drag.pointer = this._getPointer(event.gesture.center);
14894 this.drag.pinched = false;
14895 this.pinch.scale = this._getScale();
14896
14897 this._handleTouch(this.drag.pointer);
14898 };
14899
14900 /**
14901 * handle drag start event
14902 * @private
14903 */
14904 Network.prototype._onDragStart = function () {
14905 this._handleDragStart();
14906 };
14907
14908
14909 /**
14910 * This function is called by _onDragStart.
14911 * It is separated out because we can then overload it for the datamanipulation system.
14912 *
14913 * @private
14914 */
14915 Network.prototype._handleDragStart = function() {
14916 var drag = this.drag;
14917 var node = this._getNodeAt(drag.pointer);
14918 // note: drag.pointer is set in _onTouch to get the initial touch location
14919
14920 drag.dragging = true;
14921 drag.selection = [];
14922 drag.translation = this._getTranslation();
14923 drag.nodeId = null;
14924
14925 if (node != null) {
14926 drag.nodeId = node.id;
14927 // select the clicked node if not yet selected
14928 if (!node.isSelected()) {
14929 this._selectObject(node,false);
14930 }
14931
14932 this.emit("dragStart",{nodeIds:this.getSelection().nodes});
14933
14934 // create an array with the selected nodes and their original location and status
14935 for (var objectId in this.selectionObj.nodes) {
14936 if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
14937 var object = this.selectionObj.nodes[objectId];
14938 var s = {
14939 id: object.id,
14940 node: object,
14941
14942 // store original x, y, xFixed and yFixed, make the node temporarily Fixed
14943 x: object.x,
14944 y: object.y,
14945 xFixed: object.xFixed,
14946 yFixed: object.yFixed
14947 };
14948
14949 object.xFixed = true;
14950 object.yFixed = true;
14951
14952 drag.selection.push(s);
14953 }
14954 }
14955 }
14956 };
14957
14958
14959 /**
14960 * handle drag event
14961 * @private
14962 */
14963 Network.prototype._onDrag = function (event) {
14964 this._handleOnDrag(event)
14965 };
14966
14967
14968 /**
14969 * This function is called by _onDrag.
14970 * It is separated out because we can then overload it for the datamanipulation system.
14971 *
14972 * @private
14973 */
14974 Network.prototype._handleOnDrag = function(event) {
14975 if (this.drag.pinched) {
14976 return;
14977 }
14978
14979 // remove the focus on node if it is focussed on by the focusOnNode
14980 this.releaseNode();
14981
14982 var pointer = this._getPointer(event.gesture.center);
14983 var me = this;
14984 var drag = this.drag;
14985 var selection = drag.selection;
14986 if (selection && selection.length && this.constants.dragNodes == true) {
14987 // calculate delta's and new location
14988 var deltaX = pointer.x - drag.pointer.x;
14989 var deltaY = pointer.y - drag.pointer.y;
14990
14991 // update position of all selected nodes
14992 selection.forEach(function (s) {
14993 var node = s.node;
14994
14995 if (!s.xFixed) {
14996 node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
14997 }
14998
14999 if (!s.yFixed) {
15000 node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
15001 }
15002 });
15003
15004
15005 // start _animationStep if not yet running
15006 if (!this.moving) {
15007 this.moving = true;
15008 this.start();
15009 }
15010 }
15011 else {
15012 if (this.constants.dragNetwork == true) {
15013 // move the network
15014 var diffX = pointer.x - this.drag.pointer.x;
15015 var diffY = pointer.y - this.drag.pointer.y;
15016
15017 this._setTranslation(
15018 this.drag.translation.x + diffX,
15019 this.drag.translation.y + diffY
15020 );
15021 this._redraw();
15022 // this.moving = true;
15023 // this.start();
15024 }
15025 }
15026 };
15027
15028 /**
15029 * handle drag start event
15030 * @private
15031 */
15032 Network.prototype._onDragEnd = function (event) {
15033 this._handleDragEnd(event);
15034 };
15035
15036
15037 Network.prototype._handleDragEnd = function(event) {
15038 this.drag.dragging = false;
15039 var selection = this.drag.selection;
15040 if (selection && selection.length) {
15041 selection.forEach(function (s) {
15042 // restore original xFixed and yFixed
15043 s.node.xFixed = s.xFixed;
15044 s.node.yFixed = s.yFixed;
15045 });
15046 this.moving = true;
15047 this.start();
15048 }
15049 else {
15050 this._redraw();
15051 }
15052 this.emit("dragEnd",{nodeIds:this.getSelection().nodes});
15053 }
15054 /**
15055 * handle tap/click event: select/unselect a node
15056 * @private
15057 */
15058 Network.prototype._onTap = function (event) {
15059 var pointer = this._getPointer(event.gesture.center);
15060 this.pointerPosition = pointer;
15061 this._handleTap(pointer);
15062
15063 };
15064
15065
15066 /**
15067 * handle doubletap event
15068 * @private
15069 */
15070 Network.prototype._onDoubleTap = function (event) {
15071 var pointer = this._getPointer(event.gesture.center);
15072 this._handleDoubleTap(pointer);
15073 };
15074
15075
15076 /**
15077 * handle long tap event: multi select nodes
15078 * @private
15079 */
15080 Network.prototype._onHold = function (event) {
15081 var pointer = this._getPointer(event.gesture.center);
15082 this.pointerPosition = pointer;
15083 this._handleOnHold(pointer);
15084 };
15085
15086 /**
15087 * handle the release of the screen
15088 *
15089 * @private
15090 */
15091 Network.prototype._onRelease = function (event) {
15092 var pointer = this._getPointer(event.gesture.center);
15093 this._handleOnRelease(pointer);
15094 };
15095
15096 /**
15097 * Handle pinch event
15098 * @param event
15099 * @private
15100 */
15101 Network.prototype._onPinch = function (event) {
15102 var pointer = this._getPointer(event.gesture.center);
15103
15104 this.drag.pinched = true;
15105 if (!('scale' in this.pinch)) {
15106 this.pinch.scale = 1;
15107 }
15108
15109 // TODO: enabled moving while pinching?
15110 var scale = this.pinch.scale * event.gesture.scale;
15111 this._zoom(scale, pointer)
15112 };
15113
15114 /**
15115 * Zoom the network in or out
15116 * @param {Number} scale a number around 1, and between 0.01 and 10
15117 * @param {{x: Number, y: Number}} pointer Position on screen
15118 * @return {Number} appliedScale scale is limited within the boundaries
15119 * @private
15120 */
15121 Network.prototype._zoom = function(scale, pointer) {
15122 if (this.constants.zoomable == true) {
15123 var scaleOld = this._getScale();
15124 if (scale < 0.00001) {
15125 scale = 0.00001;
15126 }
15127 if (scale > 10) {
15128 scale = 10;
15129 }
15130
15131 var preScaleDragPointer = null;
15132 if (this.drag !== undefined) {
15133 if (this.drag.dragging == true) {
15134 preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer);
15135 }
15136 }
15137 // + this.frame.canvas.clientHeight / 2
15138 var translation = this._getTranslation();
15139
15140 var scaleFrac = scale / scaleOld;
15141 var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
15142 var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
15143
15144 this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
15145 "y" : this._YconvertDOMtoCanvas(pointer.y)};
15146
15147 this._setScale(scale);
15148 this._setTranslation(tx, ty);
15149 this.updateClustersDefault();
15150
15151 if (preScaleDragPointer != null) {
15152 var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer);
15153 this.drag.pointer.x = postScaleDragPointer.x;
15154 this.drag.pointer.y = postScaleDragPointer.y;
15155 }
15156
15157 this._redraw();
15158
15159 if (scaleOld < scale) {
15160 this.emit("zoom", {direction:"+"});
15161 }
15162 else {
15163 this.emit("zoom", {direction:"-"});
15164 }
15165
15166 return scale;
15167 }
15168 };
15169
15170
15171 /**
15172 * Event handler for mouse wheel event, used to zoom the timeline
15173 * See http://adomas.org/javascript-mouse-wheel/
15174 * https://github.com/EightMedia/hammer.js/issues/256
15175 * @param {MouseEvent} event
15176 * @private
15177 */
15178 Network.prototype._onMouseWheel = function(event) {
15179 // retrieve delta
15180 var delta = 0;
15181 if (event.wheelDelta) { /* IE/Opera. */
15182 delta = event.wheelDelta/120;
15183 } else if (event.detail) { /* Mozilla case. */
15184 // In Mozilla, sign of delta is different than in IE.
15185 // Also, delta is multiple of 3.
15186 delta = -event.detail/3;
15187 }
15188
15189 // If delta is nonzero, handle it.
15190 // Basically, delta is now positive if wheel was scrolled up,
15191 // and negative, if wheel was scrolled down.
15192 if (delta) {
15193
15194 // calculate the new scale
15195 var scale = this._getScale();
15196 var zoom = delta / 10;
15197 if (delta < 0) {
15198 zoom = zoom / (1 - zoom);
15199 }
15200 scale *= (1 + zoom);
15201
15202 // calculate the pointer location
15203 var gesture = hammerUtil.fakeGesture(this, event);
15204 var pointer = this._getPointer(gesture.center);
15205
15206 // apply the new scale
15207 this._zoom(scale, pointer);
15208 }
15209
15210 // Prevent default actions caused by mouse wheel.
15211 event.preventDefault();
15212 };
15213
15214
15215 /**
15216 * Mouse move handler for checking whether the title moves over a node with a title.
15217 * @param {Event} event
15218 * @private
15219 */
15220 Network.prototype._onMouseMoveTitle = function (event) {
15221 var gesture = hammerUtil.fakeGesture(this, event);
15222 var pointer = this._getPointer(gesture.center);
15223
15224 // check if the previously selected node is still selected
15225 if (this.popupObj) {
15226 this._checkHidePopup(pointer);
15227 }
15228
15229 // start a timeout that will check if the mouse is positioned above
15230 // an element
15231 var me = this;
15232 var checkShow = function() {
15233 me._checkShowPopup(pointer);
15234 };
15235 if (this.popupTimer) {
15236 clearInterval(this.popupTimer); // stop any running calculationTimer
15237 }
15238 if (!this.drag.dragging) {
15239 this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
15240 }
15241
15242
15243 /**
15244 * Adding hover highlights
15245 */
15246 if (this.constants.hover == true) {
15247 // removing all hover highlights
15248 for (var edgeId in this.hoverObj.edges) {
15249 if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
15250 this.hoverObj.edges[edgeId].hover = false;
15251 delete this.hoverObj.edges[edgeId];
15252 }
15253 }
15254
15255 // adding hover highlights
15256 var obj = this._getNodeAt(pointer);
15257 if (obj == null) {
15258 obj = this._getEdgeAt(pointer);
15259 }
15260 if (obj != null) {
15261 this._hoverObject(obj);
15262 }
15263
15264 // removing all node hover highlights except for the selected one.
15265 for (var nodeId in this.hoverObj.nodes) {
15266 if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
15267 if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
15268 this._blurObject(this.hoverObj.nodes[nodeId]);
15269 delete this.hoverObj.nodes[nodeId];
15270 }
15271 }
15272 }
15273 this.redraw();
15274 }
15275 };
15276
15277 /**
15278 * Check if there is an element on the given position in the network
15279 * (a node or edge). If so, and if this element has a title,
15280 * show a popup window with its title.
15281 *
15282 * @param {{x:Number, y:Number}} pointer
15283 * @private
15284 */
15285 Network.prototype._checkShowPopup = function (pointer) {
15286 var obj = {
15287 left: this._XconvertDOMtoCanvas(pointer.x),
15288 top: this._YconvertDOMtoCanvas(pointer.y),
15289 right: this._XconvertDOMtoCanvas(pointer.x),
15290 bottom: this._YconvertDOMtoCanvas(pointer.y)
15291 };
15292
15293 var id;
15294 var lastPopupNode = this.popupObj;
15295
15296 if (this.popupObj == undefined) {
15297 // search the nodes for overlap, select the top one in case of multiple nodes
15298 var nodes = this.nodes;
15299 for (id in nodes) {
15300 if (nodes.hasOwnProperty(id)) {
15301 var node = nodes[id];
15302 if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
15303 this.popupObj = node;
15304 break;
15305 }
15306 }
15307 }
15308 }
15309
15310 if (this.popupObj === undefined) {
15311 // search the edges for overlap
15312 var edges = this.edges;
15313 for (id in edges) {
15314 if (edges.hasOwnProperty(id)) {
15315 var edge = edges[id];
15316 if (edge.connected && (edge.getTitle() !== undefined) &&
15317 edge.isOverlappingWith(obj)) {
15318 this.popupObj = edge;
15319 break;
15320 }
15321 }
15322 }
15323 }
15324
15325 if (this.popupObj) {
15326 // show popup message window
15327 if (this.popupObj != lastPopupNode) {
15328 var me = this;
15329 if (!me.popup) {
15330 me.popup = new Popup(me.frame, me.constants.tooltip);
15331 }
15332
15333 // adjust a small offset such that the mouse cursor is located in the
15334 // bottom left location of the popup, and you can easily move over the
15335 // popup area
15336 me.popup.setPosition(pointer.x - 3, pointer.y - 3);
15337 me.popup.setText(me.popupObj.getTitle());
15338 me.popup.show();
15339 }
15340 }
15341 else {
15342 if (this.popup) {
15343 this.popup.hide();
15344 }
15345 }
15346 };
15347
15348
15349 /**
15350 * Check if the popup must be hided, which is the case when the mouse is no
15351 * longer hovering on the object
15352 * @param {{x:Number, y:Number}} pointer
15353 * @private
15354 */
15355 Network.prototype._checkHidePopup = function (pointer) {
15356 if (!this.popupObj || !this._getNodeAt(pointer) ) {
15357 this.popupObj = undefined;
15358 if (this.popup) {
15359 this.popup.hide();
15360 }
15361 }
15362 };
15363
15364
15365 /**
15366 * Set a new size for the network
15367 * @param {string} width Width in pixels or percentage (for example '800px'
15368 * or '50%')
15369 * @param {string} height Height in pixels or percentage (for example '400px'
15370 * or '30%')
15371 */
15372 Network.prototype.setSize = function(width, height) {
15373 var emitEvent = false;
15374 if (width != this.constants.width || height != this.constants.height || this.frame.style.width != width || this.frame.style.height != height) {
15375 this.frame.style.width = width;
15376 this.frame.style.height = height;
15377
15378 this.frame.canvas.style.width = '100%';
15379 this.frame.canvas.style.height = '100%';
15380
15381 this.frame.canvas.width = this.frame.canvas.clientWidth;
15382 this.frame.canvas.height = this.frame.canvas.clientHeight;
15383
15384 this.constants.width = width;
15385 this.constants.height = height;
15386
15387 emitEvent = true;
15388 }
15389 else {
15390 // this would adapt the width of the canvas to the width from 100% if and only if
15391 // there is a change.
15392
15393 if (this.frame.canvas.width != this.frame.canvas.clientWidth) {
15394 this.frame.canvas.width = this.frame.canvas.clientWidth;
15395 emitEvent = true;
15396 }
15397 if (this.frame.canvas.height != this.frame.canvas.clientHeight) {
15398 this.frame.canvas.height = this.frame.canvas.clientHeight;
15399 emitEvent = true;
15400 }
15401 }
15402
15403 if (emitEvent == true) {
15404 this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
15405 }
15406 };
15407
15408 /**
15409 * Set a data set with nodes for the network
15410 * @param {Array | DataSet | DataView} nodes The data containing the nodes.
15411 * @private
15412 */
15413 Network.prototype._setNodes = function(nodes) {
15414 var oldNodesData = this.nodesData;
15415
15416 if (nodes instanceof DataSet || nodes instanceof DataView) {
15417 this.nodesData = nodes;
15418 }
15419 else if (nodes instanceof Array) {
15420 this.nodesData = new DataSet();
15421 this.nodesData.add(nodes);
15422 }
15423 else if (!nodes) {
15424 this.nodesData = new DataSet();
15425 }
15426 else {
15427 throw new TypeError('Array or DataSet expected');
15428 }
15429
15430 if (oldNodesData) {
15431 // unsubscribe from old dataset
15432 util.forEach(this.nodesListeners, function (callback, event) {
15433 oldNodesData.off(event, callback);
15434 });
15435 }
15436
15437 // remove drawn nodes
15438 this.nodes = {};
15439
15440 if (this.nodesData) {
15441 // subscribe to new dataset
15442 var me = this;
15443 util.forEach(this.nodesListeners, function (callback, event) {
15444 me.nodesData.on(event, callback);
15445 });
15446
15447 // draw all new nodes
15448 var ids = this.nodesData.getIds();
15449 this._addNodes(ids);
15450 }
15451 this._updateSelection();
15452 };
15453
15454 /**
15455 * Add nodes
15456 * @param {Number[] | String[]} ids
15457 * @private
15458 */
15459 Network.prototype._addNodes = function(ids) {
15460 var id;
15461 for (var i = 0, len = ids.length; i < len; i++) {
15462 id = ids[i];
15463 var data = this.nodesData.get(id);
15464 var node = new Node(data, this.images, this.groups, this.constants);
15465 this.nodes[id] = node; // note: this may replace an existing node
15466 if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
15467 var radius = 10 * 0.1*ids.length + 10;
15468 var angle = 2 * Math.PI * Math.random();
15469 if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
15470 if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
15471 }
15472 this.moving = true;
15473 }
15474
15475 this._updateNodeIndexList();
15476 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15477 this._resetLevels();
15478 this._setupHierarchicalLayout();
15479 }
15480 this._updateCalculationNodes();
15481 this._reconnectEdges();
15482 this._updateValueRange(this.nodes);
15483 this.updateLabels();
15484 };
15485
15486 /**
15487 * Update existing nodes, or create them when not yet existing
15488 * @param {Number[] | String[]} ids
15489 * @private
15490 */
15491 Network.prototype._updateNodes = function(ids) {
15492 var nodes = this.nodes,
15493 nodesData = this.nodesData;
15494 for (var i = 0, len = ids.length; i < len; i++) {
15495 var id = ids[i];
15496 var node = nodes[id];
15497 var data = nodesData.get(id);
15498 if (node) {
15499 // update node
15500 node.setProperties(data, this.constants);
15501 }
15502 else {
15503 // create node
15504 node = new Node(properties, this.images, this.groups, this.constants);
15505 nodes[id] = node;
15506 }
15507 }
15508 this.moving = true;
15509 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15510 this._resetLevels();
15511 this._setupHierarchicalLayout();
15512 }
15513 this._updateNodeIndexList();
15514 this._reconnectEdges();
15515 this._updateValueRange(nodes);
15516 };
15517
15518 /**
15519 * Remove existing nodes. If nodes do not exist, the method will just ignore it.
15520 * @param {Number[] | String[]} ids
15521 * @private
15522 */
15523 Network.prototype._removeNodes = function(ids) {
15524 var nodes = this.nodes;
15525 for (var i = 0, len = ids.length; i < len; i++) {
15526 var id = ids[i];
15527 delete nodes[id];
15528 }
15529 this._updateNodeIndexList();
15530 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15531 this._resetLevels();
15532 this._setupHierarchicalLayout();
15533 }
15534 this._updateCalculationNodes();
15535 this._reconnectEdges();
15536 this._updateSelection();
15537 this._updateValueRange(nodes);
15538 };
15539
15540 /**
15541 * Load edges by reading the data table
15542 * @param {Array | DataSet | DataView} edges The data containing the edges.
15543 * @private
15544 * @private
15545 */
15546 Network.prototype._setEdges = function(edges) {
15547 var oldEdgesData = this.edgesData;
15548
15549 if (edges instanceof DataSet || edges instanceof DataView) {
15550 this.edgesData = edges;
15551 }
15552 else if (edges instanceof Array) {
15553 this.edgesData = new DataSet();
15554 this.edgesData.add(edges);
15555 }
15556 else if (!edges) {
15557 this.edgesData = new DataSet();
15558 }
15559 else {
15560 throw new TypeError('Array or DataSet expected');
15561 }
15562
15563 if (oldEdgesData) {
15564 // unsubscribe from old dataset
15565 util.forEach(this.edgesListeners, function (callback, event) {
15566 oldEdgesData.off(event, callback);
15567 });
15568 }
15569
15570 // remove drawn edges
15571 this.edges = {};
15572
15573 if (this.edgesData) {
15574 // subscribe to new dataset
15575 var me = this;
15576 util.forEach(this.edgesListeners, function (callback, event) {
15577 me.edgesData.on(event, callback);
15578 });
15579
15580 // draw all new nodes
15581 var ids = this.edgesData.getIds();
15582 this._addEdges(ids);
15583 }
15584
15585 this._reconnectEdges();
15586 };
15587
15588 /**
15589 * Add edges
15590 * @param {Number[] | String[]} ids
15591 * @private
15592 */
15593 Network.prototype._addEdges = function (ids) {
15594 var edges = this.edges,
15595 edgesData = this.edgesData;
15596
15597 for (var i = 0, len = ids.length; i < len; i++) {
15598 var id = ids[i];
15599
15600 var oldEdge = edges[id];
15601 if (oldEdge) {
15602 oldEdge.disconnect();
15603 }
15604
15605 var data = edgesData.get(id, {"showInternalIds" : true});
15606 edges[id] = new Edge(data, this, this.constants);
15607 }
15608 this.moving = true;
15609 this._updateValueRange(edges);
15610 this._createBezierNodes();
15611 this._updateCalculationNodes();
15612 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15613 this._resetLevels();
15614 this._setupHierarchicalLayout();
15615 }
15616 };
15617
15618 /**
15619 * Update existing edges, or create them when not yet existing
15620 * @param {Number[] | String[]} ids
15621 * @private
15622 */
15623 Network.prototype._updateEdges = function (ids) {
15624 var edges = this.edges,
15625 edgesData = this.edgesData;
15626 for (var i = 0, len = ids.length; i < len; i++) {
15627 var id = ids[i];
15628
15629 var data = edgesData.get(id);
15630 var edge = edges[id];
15631 if (edge) {
15632 // update edge
15633 edge.disconnect();
15634 edge.setProperties(data, this.constants);
15635 edge.connect();
15636 }
15637 else {
15638 // create edge
15639 edge = new Edge(data, this, this.constants);
15640 this.edges[id] = edge;
15641 }
15642 }
15643
15644 this._createBezierNodes();
15645 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15646 this._resetLevels();
15647 this._setupHierarchicalLayout();
15648 }
15649 this.moving = true;
15650 this._updateValueRange(edges);
15651 };
15652
15653 /**
15654 * Remove existing edges. Non existing ids will be ignored
15655 * @param {Number[] | String[]} ids
15656 * @private
15657 */
15658 Network.prototype._removeEdges = function (ids) {
15659 var edges = this.edges;
15660 for (var i = 0, len = ids.length; i < len; i++) {
15661 var id = ids[i];
15662 var edge = edges[id];
15663 if (edge) {
15664 if (edge.via != null) {
15665 delete this.sectors['support']['nodes'][edge.via.id];
15666 }
15667 edge.disconnect();
15668 delete edges[id];
15669 }
15670 }
15671
15672 this.moving = true;
15673 this._updateValueRange(edges);
15674 if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
15675 this._resetLevels();
15676 this._setupHierarchicalLayout();
15677 }
15678 this._updateCalculationNodes();
15679 };
15680
15681 /**
15682 * Reconnect all edges
15683 * @private
15684 */
15685 Network.prototype._reconnectEdges = function() {
15686 var id,
15687 nodes = this.nodes,
15688 edges = this.edges;
15689 for (id in nodes) {
15690 if (nodes.hasOwnProperty(id)) {
15691 nodes[id].edges = [];
15692 nodes[id].dynamicEdges = [];
15693 }
15694 }
15695
15696 for (id in edges) {
15697 if (edges.hasOwnProperty(id)) {
15698 var edge = edges[id];
15699 edge.from = null;
15700 edge.to = null;
15701 edge.connect();
15702 }
15703 }
15704 };
15705
15706 /**
15707 * Update the values of all object in the given array according to the current
15708 * value range of the objects in the array.
15709 * @param {Object} obj An object containing a set of Edges or Nodes
15710 * The objects must have a method getValue() and
15711 * setValueRange(min, max).
15712 * @private
15713 */
15714 Network.prototype._updateValueRange = function(obj) {
15715 var id;
15716
15717 // determine the range of the objects
15718 var valueMin = undefined;
15719 var valueMax = undefined;
15720 for (id in obj) {
15721 if (obj.hasOwnProperty(id)) {
15722 var value = obj[id].getValue();
15723 if (value !== undefined) {
15724 valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
15725 valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
15726 }
15727 }
15728 }
15729
15730 // adjust the range of all objects
15731 if (valueMin !== undefined && valueMax !== undefined) {
15732 for (id in obj) {
15733 if (obj.hasOwnProperty(id)) {
15734 obj[id].setValueRange(valueMin, valueMax);
15735 }
15736 }
15737 }
15738 };
15739
15740 /**
15741 * Redraw the network with the current data
15742 * chart will be resized too.
15743 */
15744 Network.prototype.redraw = function() {
15745 this.setSize(this.constants.width, this.constants.height);
15746 this._redraw();
15747 };
15748
15749 /**
15750 * Redraw the network with the current data
15751 * @private
15752 */
15753 Network.prototype._redraw = function() {
15754 var ctx = this.frame.canvas.getContext('2d');
15755 // clear the canvas
15756 var w = this.frame.canvas.width;
15757 var h = this.frame.canvas.height;
15758 ctx.clearRect(0, 0, w, h);
15759
15760 // set scaling and translation
15761 ctx.save();
15762 ctx.translate(this.translation.x, this.translation.y);
15763 ctx.scale(this.scale, this.scale);
15764
15765 this.canvasTopLeft = {
15766 "x": this._XconvertDOMtoCanvas(0),
15767 "y": this._YconvertDOMtoCanvas(0)
15768 };
15769 this.canvasBottomRight = {
15770 "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
15771 "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
15772 };
15773
15774
15775 this._doInAllSectors("_drawAllSectorNodes",ctx);
15776 if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
15777 this._doInAllSectors("_drawEdges",ctx);
15778 }
15779
15780 if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
15781 this._doInAllSectors("_drawNodes",ctx,false);
15782 }
15783
15784 if (this.controlNodesActive == true) {
15785 this._doInAllSectors("_drawControlNodes",ctx);
15786 }
15787
15788 // this._doInSupportSector("_drawNodes",ctx,true);
15789 // this._drawTree(ctx,"#F00F0F");
15790
15791 // restore original scaling and translation
15792 ctx.restore();
15793 };
15794
15795 /**
15796 * Set the translation of the network
15797 * @param {Number} offsetX Horizontal offset
15798 * @param {Number} offsetY Vertical offset
15799 * @private
15800 */
15801 Network.prototype._setTranslation = function(offsetX, offsetY) {
15802 if (this.translation === undefined) {
15803 this.translation = {
15804 x: 0,
15805 y: 0
15806 };
15807 }
15808
15809 if (offsetX !== undefined) {
15810 this.translation.x = offsetX;
15811 }
15812 if (offsetY !== undefined) {
15813 this.translation.y = offsetY;
15814 }
15815
15816 this.emit('viewChanged');
15817 };
15818
15819 /**
15820 * Get the translation of the network
15821 * @return {Object} translation An object with parameters x and y, both a number
15822 * @private
15823 */
15824 Network.prototype._getTranslation = function() {
15825 return {
15826 x: this.translation.x,
15827 y: this.translation.y
15828 };
15829 };
15830
15831 /**
15832 * Scale the network
15833 * @param {Number} scale Scaling factor 1.0 is unscaled
15834 * @private
15835 */
15836 Network.prototype._setScale = function(scale) {
15837 this.scale = scale;
15838 };
15839
15840 /**
15841 * Get the current scale of the network
15842 * @return {Number} scale Scaling factor 1.0 is unscaled
15843 * @private
15844 */
15845 Network.prototype._getScale = function() {
15846 return this.scale;
15847 };
15848
15849 /**
15850 * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
15851 * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
15852 * @param {number} x
15853 * @returns {number}
15854 * @private
15855 */
15856 Network.prototype._XconvertDOMtoCanvas = function(x) {
15857 return (x - this.translation.x) / this.scale;
15858 };
15859
15860 /**
15861 * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
15862 * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
15863 * @param {number} x
15864 * @returns {number}
15865 * @private
15866 */
15867 Network.prototype._XconvertCanvasToDOM = function(x) {
15868 return x * this.scale + this.translation.x;
15869 };
15870
15871 /**
15872 * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
15873 * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
15874 * @param {number} y
15875 * @returns {number}
15876 * @private
15877 */
15878 Network.prototype._YconvertDOMtoCanvas = function(y) {
15879 return (y - this.translation.y) / this.scale;
15880 };
15881
15882 /**
15883 * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
15884 * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
15885 * @param {number} y
15886 * @returns {number}
15887 * @private
15888 */
15889 Network.prototype._YconvertCanvasToDOM = function(y) {
15890 return y * this.scale + this.translation.y ;
15891 };
15892
15893
15894 /**
15895 *
15896 * @param {object} pos = {x: number, y: number}
15897 * @returns {{x: number, y: number}}
15898 * @constructor
15899 */
15900 Network.prototype.canvasToDOM = function (pos) {
15901 return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)};
15902 };
15903
15904 /**
15905 *
15906 * @param {object} pos = {x: number, y: number}
15907 * @returns {{x: number, y: number}}
15908 * @constructor
15909 */
15910 Network.prototype.DOMtoCanvas = function (pos) {
15911 return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)};
15912 };
15913
15914 /**
15915 * Redraw all nodes
15916 * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
15917 * @param {CanvasRenderingContext2D} ctx
15918 * @param {Boolean} [alwaysShow]
15919 * @private
15920 */
15921 Network.prototype._drawNodes = function(ctx,alwaysShow) {
15922 if (alwaysShow === undefined) {
15923 alwaysShow = false;
15924 }
15925
15926 // first draw the unselected nodes
15927 var nodes = this.nodes;
15928 var selected = [];
15929
15930 for (var id in nodes) {
15931 if (nodes.hasOwnProperty(id)) {
15932 nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
15933 if (nodes[id].isSelected()) {
15934 selected.push(id);
15935 }
15936 else {
15937 if (nodes[id].inArea() || alwaysShow) {
15938 nodes[id].draw(ctx);
15939 }
15940 }
15941 }
15942 }
15943
15944 // draw the selected nodes on top
15945 for (var s = 0, sMax = selected.length; s < sMax; s++) {
15946 if (nodes[selected[s]].inArea() || alwaysShow) {
15947 nodes[selected[s]].draw(ctx);
15948 }
15949 }
15950 };
15951
15952 /**
15953 * Redraw all edges
15954 * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
15955 * @param {CanvasRenderingContext2D} ctx
15956 * @private
15957 */
15958 Network.prototype._drawEdges = function(ctx) {
15959 var edges = this.edges;
15960 for (var id in edges) {
15961 if (edges.hasOwnProperty(id)) {
15962 var edge = edges[id];
15963 edge.setScale(this.scale);
15964 if (edge.connected) {
15965 edges[id].draw(ctx);
15966 }
15967 }
15968 }
15969 };
15970
15971 /**
15972 * Redraw all edges
15973 * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
15974 * @param {CanvasRenderingContext2D} ctx
15975 * @private
15976 */
15977 Network.prototype._drawControlNodes = function(ctx) {
15978 var edges = this.edges;
15979 for (var id in edges) {
15980 if (edges.hasOwnProperty(id)) {
15981 edges[id]._drawControlNodes(ctx);
15982 }
15983 }
15984 };
15985
15986 /**
15987 * Find a stable position for all nodes
15988 * @private
15989 */
15990 Network.prototype._stabilize = function() {
15991 if (this.constants.freezeForStabilization == true) {
15992 this._freezeDefinedNodes();
15993 }
15994
15995 // find stable position
15996 var count = 0;
15997 while (this.moving && count < this.constants.stabilizationIterations) {
15998 this._physicsTick();
15999 count++;
16000 }
16001 this.zoomExtent(undefined,false,true);
16002 if (this.constants.freezeForStabilization == true) {
16003 this._restoreFrozenNodes();
16004 }
16005 };
16006
16007 /**
16008 * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
16009 * because only the supportnodes for the smoothCurves have to settle.
16010 *
16011 * @private
16012 */
16013 Network.prototype._freezeDefinedNodes = function() {
16014 var nodes = this.nodes;
16015 for (var id in nodes) {
16016 if (nodes.hasOwnProperty(id)) {
16017 if (nodes[id].x != null && nodes[id].y != null) {
16018 nodes[id].fixedData.x = nodes[id].xFixed;
16019 nodes[id].fixedData.y = nodes[id].yFixed;
16020 nodes[id].xFixed = true;
16021 nodes[id].yFixed = true;
16022 }
16023 }
16024 }
16025 };
16026
16027 /**
16028 * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
16029 *
16030 * @private
16031 */
16032 Network.prototype._restoreFrozenNodes = function() {
16033 var nodes = this.nodes;
16034 for (var id in nodes) {
16035 if (nodes.hasOwnProperty(id)) {
16036 if (nodes[id].fixedData.x != null) {
16037 nodes[id].xFixed = nodes[id].fixedData.x;
16038 nodes[id].yFixed = nodes[id].fixedData.y;
16039 }
16040 }
16041 }
16042 };
16043
16044
16045 /**
16046 * Check if any of the nodes is still moving
16047 * @param {number} vmin the minimum velocity considered as 'moving'
16048 * @return {boolean} true if moving, false if non of the nodes is moving
16049 * @private
16050 */
16051 Network.prototype._isMoving = function(vmin) {
16052 var nodes = this.nodes;
16053 for (var id in nodes) {
16054 if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
16055 return true;
16056 }
16057 }
16058 return false;
16059 };
16060
16061
16062 /**
16063 * /**
16064 * Perform one discrete step for all nodes
16065 *
16066 * @private
16067 */
16068 Network.prototype._discreteStepNodes = function() {
16069 var interval = this.physicsDiscreteStepsize;
16070 var nodes = this.nodes;
16071 var nodeId;
16072 var nodesPresent = false;
16073
16074 if (this.constants.maxVelocity > 0) {
16075 for (nodeId in nodes) {
16076 if (nodes.hasOwnProperty(nodeId)) {
16077 nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
16078 nodesPresent = true;
16079 }
16080 }
16081 }
16082 else {
16083 for (nodeId in nodes) {
16084 if (nodes.hasOwnProperty(nodeId)) {
16085 nodes[nodeId].discreteStep(interval);
16086 nodesPresent = true;
16087 }
16088 }
16089 }
16090
16091 if (nodesPresent == true) {
16092 var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
16093 if (vminCorrected > 0.5*this.constants.maxVelocity) {
16094 return true;
16095 }
16096 else {
16097 return this._isMoving(vminCorrected);
16098 }
16099 }
16100 return false;
16101 };
16102
16103 /**
16104 * A single simulation step (or "tick") in the physics simulation
16105 *
16106 * @private
16107 */
16108 Network.prototype._physicsTick = function() {
16109 if (!this.freezeSimulation) {
16110 if (this.moving == true) {
16111 var mainMovingStatus = false;
16112 var supportMovingStatus = false;
16113
16114 this._doInAllActiveSectors("_initializeForceCalculation");
16115 var mainMoving = this._doInAllActiveSectors("_discreteStepNodes");
16116 if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
16117 supportMovingStatus = this._doInSupportSector("_discreteStepNodes");
16118 }
16119 // gather movement data from all sectors, if one moves, we are NOT stabilzied
16120 for (var i = 0; i < mainMoving.length; i++) {mainMovingStatus = mainMoving[0] || mainMovingStatus;}
16121
16122 // determine if the network has stabilzied
16123 this.moving = mainMovingStatus || supportMovingStatus;
16124
16125 this.stabilizationIterations++;
16126 }
16127 }
16128 };
16129
16130
16131 /**
16132 * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
16133 * It reschedules itself at the beginning of the function
16134 *
16135 * @private
16136 */
16137 Network.prototype._animationStep = function() {
16138 // reset the timer so a new scheduled animation step can be set
16139 this.timer = undefined;
16140 // handle the keyboad movement
16141 this._handleNavigation();
16142
16143 // this schedules a new animation step
16144 this.start();
16145
16146 // start the physics simulation
16147 var calculationTime = Date.now();
16148 var maxSteps = 1;
16149 this._physicsTick();
16150 var timeRequired = Date.now() - calculationTime;
16151 while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
16152 this._physicsTick();
16153 timeRequired = Date.now() - calculationTime;
16154 maxSteps++;
16155 }
16156 // start the rendering process
16157 var renderTime = Date.now();
16158 this._redraw();
16159 this.renderTime = Date.now() - renderTime;
16160 };
16161
16162 if (typeof window !== 'undefined') {
16163 window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
16164 window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
16165 }
16166
16167 /**
16168 * Schedule a animation step with the refreshrate interval.
16169 */
16170 Network.prototype.start = function() {
16171 if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
16172 if (!this.timer) {
16173 var ua = navigator.userAgent.toLowerCase();
16174
16175 var requiresTimeout = false;
16176 if (ua.indexOf('msie 9.0') != -1) { // IE 9
16177 requiresTimeout = true;
16178 }
16179 else if (ua.indexOf('safari') != -1) { // safari
16180 if (ua.indexOf('chrome') <= -1) {
16181 requiresTimeout = true;
16182 }
16183 }
16184
16185 if (requiresTimeout == true) {
16186 this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
16187 }
16188 else{
16189 this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
16190 }
16191 }
16192 }
16193 else {
16194 this._redraw();
16195 if (this.stabilizationIterations > 0) {
16196 // trigger the "stabilized" event.
16197 // The event is triggered on the next tick, to prevent the case that
16198 // it is fired while initializing the Network, in which case you would not
16199 // be able to catch it
16200 var me = this;
16201 var params = {
16202 iterations: me.stabilizationIterations
16203 };
16204 me.stabilizationIterations = 0;
16205 setTimeout(function () {
16206 me.emit("stabilized", params);
16207 }, 0);
16208 }
16209 }
16210 };
16211
16212
16213 /**
16214 * Move the network according to the keyboard presses.
16215 *
16216 * @private
16217 */
16218 Network.prototype._handleNavigation = function() {
16219 if (this.xIncrement != 0 || this.yIncrement != 0) {
16220 var translation = this._getTranslation();
16221 this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
16222 }
16223 if (this.zoomIncrement != 0) {
16224 var center = {
16225 x: this.frame.canvas.clientWidth / 2,
16226 y: this.frame.canvas.clientHeight / 2
16227 };
16228 this._zoom(this.scale*(1 + this.zoomIncrement), center);
16229 }
16230 };
16231
16232
16233 /**
16234 * Freeze the _animationStep
16235 */
16236 Network.prototype.toggleFreeze = function() {
16237 if (this.freezeSimulation == false) {
16238 this.freezeSimulation = true;
16239 }
16240 else {
16241 this.freezeSimulation = false;
16242 this.start();
16243 }
16244 };
16245
16246
16247 /**
16248 * This function cleans the support nodes if they are not needed and adds them when they are.
16249 *
16250 * @param {boolean} [disableStart]
16251 * @private
16252 */
16253 Network.prototype._configureSmoothCurves = function(disableStart) {
16254 if (disableStart === undefined) {
16255 disableStart = true;
16256 }
16257 if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
16258 this._createBezierNodes();
16259 // cleanup unused support nodes
16260 for (var nodeId in this.sectors['support']['nodes']) {
16261 if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) {
16262 if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) {
16263 delete this.sectors['support']['nodes'][nodeId];
16264 }
16265 }
16266 }
16267 }
16268 else {
16269 // delete the support nodes
16270 this.sectors['support']['nodes'] = {};
16271 for (var edgeId in this.edges) {
16272 if (this.edges.hasOwnProperty(edgeId)) {
16273 this.edges[edgeId].via = null;
16274 }
16275 }
16276 }
16277
16278
16279 this._updateCalculationNodes();
16280 if (!disableStart) {
16281 this.moving = true;
16282 this.start();
16283 }
16284 };
16285
16286
16287 /**
16288 * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
16289 * are used for the force calculation.
16290 *
16291 * @private
16292 */
16293 Network.prototype._createBezierNodes = function() {
16294 if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
16295 for (var edgeId in this.edges) {
16296 if (this.edges.hasOwnProperty(edgeId)) {
16297 var edge = this.edges[edgeId];
16298 if (edge.via == null) {
16299 var nodeId = "edgeId:".concat(edge.id);
16300 this.sectors['support']['nodes'][nodeId] = new Node(
16301 {id:nodeId,
16302 mass:1,
16303 shape:'circle',
16304 image:"",
16305 internalMultiplier:1
16306 },{},{},this.constants);
16307 edge.via = this.sectors['support']['nodes'][nodeId];
16308 edge.via.parentEdgeId = edge.id;
16309 edge.positionBezierNode();
16310 }
16311 }
16312 }
16313 }
16314 };
16315
16316 /**
16317 * load the functions that load the mixins into the prototype.
16318 *
16319 * @private
16320 */
16321 Network.prototype._initializeMixinLoaders = function () {
16322 for (var mixin in MixinLoader) {
16323 if (MixinLoader.hasOwnProperty(mixin)) {
16324 Network.prototype[mixin] = MixinLoader[mixin];
16325 }
16326 }
16327 };
16328
16329 /**
16330 * Load the XY positions of the nodes into the dataset.
16331 */
16332 Network.prototype.storePosition = function() {
16333 var dataArray = [];
16334 for (var nodeId in this.nodes) {
16335 if (this.nodes.hasOwnProperty(nodeId)) {
16336 var node = this.nodes[nodeId];
16337 var allowedToMoveX = !this.nodes.xFixed;
16338 var allowedToMoveY = !this.nodes.yFixed;
16339 if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) {
16340 dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
16341 }
16342 }
16343 }
16344 this.nodesData.update(dataArray);
16345 };
16346
16347
16348 /**
16349 * Center a node in view.
16350 *
16351 * @param {Number} nodeId
16352 * @param {Number} [options]
16353 */
16354 Network.prototype.focusOnNode = function (nodeId, options) {
16355 if (this.nodes.hasOwnProperty(nodeId)) {
16356 if (options === undefined) {
16357 options = {};
16358 }
16359 var nodePosition = {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
16360 options.position = nodePosition;
16361 options.lockedOnNode = nodeId;
16362
16363 this.moveTo(options)
16364 }
16365 else {
16366 console.log("This nodeId cannot be found.");
16367 }
16368 };
16369
16370 /**
16371 *
16372 * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
16373 * | options.scale = Number // scale to move to
16374 * | options.position = {x:Number, y:Number} // position to move to
16375 * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to
16376 */
16377 Network.prototype.moveTo = function (options) {
16378 if (options === undefined) {
16379 options = {};
16380 return;
16381 }
16382 if (options.offset === undefined) {options.offset = {x: 0, y: 0}; }
16383 if (options.offset.x === undefined) {options.offset.x = 0; }
16384 if (options.offset.y === undefined) {options.offset.y = 0; }
16385 if (options.scale === undefined) {options.scale = this._getScale(); }
16386 if (options.position === undefined) {options.position = this._getTranslation();}
16387 if (options.animation === undefined) {options.animation = {duration:0}; }
16388 if (options.animation === false ) {options.animation = {duration:0}; }
16389 if (options.animation === true ) {options.animation = {}; }
16390 if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration
16391 if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function
16392
16393 this.animateView(options);
16394 };
16395
16396 /**
16397 *
16398 * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
16399 * | options.time = Number // animation time in milliseconds
16400 * | options.scale = Number // scale to animate to
16401 * | options.position = {x:Number, y:Number} // position to animate to
16402 * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad,
16403 * // easeInCubic, easeOutCubic, easeInOutCubic,
16404 * // easeInQuart, easeOutQuart, easeInOutQuart,
16405 * // easeInQuint, easeOutQuint, easeInOutQuint
16406 */
16407 Network.prototype.animateView = function (options) {
16408 if (options === undefined) {
16409 options = {};
16410 return;
16411 }
16412
16413 // release if something focussed on the node
16414 this.releaseNode();
16415 if (options.locked == true) {
16416 this.lockedOnNodeId = options.lockedOnNode;
16417 this.lockedOnNodeOffset = options.offset;
16418 }
16419
16420 // forcefully complete the old animation if it was still running
16421 if (this.easingTime != 0) {
16422 this._transitionRedraw(1); // by setting easingtime to 1, we finish the animation.
16423 }
16424
16425 this.sourceScale = this._getScale();
16426 this.sourceTranslation = this._getTranslation();
16427 this.targetScale = options.scale;
16428
16429 // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
16430 // but at least then we'll have the target transition
16431 this._setScale(this.targetScale);
16432 var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
16433 var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
16434 x: viewCenter.x - options.position.x,
16435 y: viewCenter.y - options.position.y
16436 };
16437 this.targetTranslation = {
16438 x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x,
16439 y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y
16440 };
16441
16442 // if the time is set to 0, don't do an animation
16443 if (options.animation.duration == 0) {
16444 if (this.lockedOnNodeId != null) {
16445 this._classicRedraw = this._redraw;
16446 this._redraw = this._lockedRedraw;
16447 }
16448 else {
16449 this._setScale(this.targetScale);
16450 this._setTranslation(this.targetTranslation.x, this.targetTranslation.y);
16451 this._redraw();
16452 }
16453 }
16454 else {
16455 this.animationSpeed = 1 / (this.renderRefreshRate * options.animation.duration * 0.001) || 1 / this.renderRefreshRate;
16456 this.animationEasingFunction = options.animation.easingFunction;
16457 this._classicRedraw = this._redraw;
16458 this._redraw = this._transitionRedraw;
16459 this._redraw();
16460 this.moving = true;
16461 this.start();
16462 }
16463 };
16464
16465
16466 Network.prototype._lockedRedraw = function () {
16467 var nodePosition = {x: this.nodes[this.lockedOnNodeId].x, y: this.nodes[this.lockedOnNodeId].y};
16468 var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
16469 var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
16470 x: viewCenter.x - nodePosition.x,
16471 y: viewCenter.y - nodePosition.y
16472 };
16473 var sourceTranslation = this._getTranslation();
16474 var targetTranslation = {
16475 x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x,
16476 y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y
16477 };
16478
16479 this._setTranslation(targetTranslation.x,targetTranslation.y);
16480 this._classicRedraw();
16481 }
16482
16483 Network.prototype.releaseNode = function () {
16484 if (this.lockedOnNodeId != null) {
16485 this._redraw = this._classicRedraw;
16486 this.lockedOnNodeId = null;
16487 this.lockedOnNodeOffset = null;
16488 }
16489 }
16490
16491 /**
16492 *
16493 * @param easingTime
16494 * @private
16495 */
16496 Network.prototype._transitionRedraw = function (easingTime) {
16497 this.easingTime = easingTime || this.easingTime + this.animationSpeed;
16498 this.easingTime += this.animationSpeed;
16499
16500 var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);
16501
16502 this._setScale(this.sourceScale + (this.targetScale - this.sourceScale) * progress);
16503 this._setTranslation(
16504 this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress,
16505 this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress
16506 );
16507
16508 this._classicRedraw();
16509 this.moving = true;
16510
16511 // cleanup
16512 if (this.easingTime >= 1.0) {
16513 this.easingTime = 0;
16514 if (this.lockedOnNodeId != null) {
16515 this._redraw = this._lockedRedraw;
16516 }
16517 else {
16518 this._redraw = this._classicRedraw;
16519 }
16520 this.emit("animationFinished");
16521 }
16522 };
16523
16524 Network.prototype._classicRedraw = function () {
16525 // placeholder function to be overloaded by animations;
16526 };
16527
16528 /**
16529 * Returns true when the Network is active.
16530 * @returns {boolean}
16531 */
16532 Network.prototype.isActive = function () {
16533 return !this.activator || this.activator.active;
16534 };
16535
16536
16537 /**
16538 * Sets the scale
16539 * @returns {Number}
16540 */
16541 Network.prototype.setScale = function () {
16542 return this._setScale();
16543 };
16544
16545
16546 /**
16547 * Returns the scale
16548 * @returns {Number}
16549 */
16550 Network.prototype.getScale = function () {
16551 return this._getScale();
16552 };
16553
16554
16555
16556 module.exports = Network;
16557
16558
16559/***/ },
16560/* 34 */
16561/***/ function(module, exports, __webpack_require__) {
16562
16563 var util = __webpack_require__(1);
16564 var Node = __webpack_require__(37);
16565
16566 /**
16567 * @class Edge
16568 *
16569 * A edge connects two nodes
16570 * @param {Object} properties Object with properties. Must contain
16571 * At least properties from and to.
16572 * Available properties: from (number),
16573 * to (number), label (string, color (string),
16574 * width (number), style (string),
16575 * length (number), title (string)
16576 * @param {Network} network A Network object, used to find and edge to
16577 * nodes.
16578 * @param {Object} constants An object with default values for
16579 * example for the color
16580 */
16581 function Edge (properties, network, networkConstants) {
16582 if (!network) {
16583 throw "No network provided";
16584 }
16585 var fields = ['edges','physics'];
16586 var constants = util.selectiveBridgeObject(fields,networkConstants);
16587 this.options = constants.edges;
16588 this.physics = constants.physics;
16589 this.options['smoothCurves'] = networkConstants['smoothCurves'];
16590
16591
16592 this.network = network;
16593
16594 // initialize variables
16595 this.id = undefined;
16596 this.fromId = undefined;
16597 this.toId = undefined;
16598 this.title = undefined;
16599 this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
16600 this.value = undefined;
16601 this.selected = false;
16602 this.hover = false;
16603 this.labelDimensions = {top:0,left:0,width:0,height:0};
16604
16605 this.from = null; // a node
16606 this.to = null; // a node
16607 this.via = null; // a temp node
16608
16609 // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
16610 // by storing the original information we can revert to the original connection when the cluser is opened.
16611 this.originalFromId = [];
16612 this.originalToId = [];
16613
16614 this.connected = false;
16615
16616 this.widthFixed = false;
16617 this.lengthFixed = false;
16618
16619 this.setProperties(properties);
16620
16621 this.controlNodesEnabled = false;
16622 this.controlNodes = {from:null, to:null, positions:{}};
16623 this.connectedNode = null;
16624 }
16625
16626 /**
16627 * Set or overwrite properties for the edge
16628 * @param {Object} properties an object with properties
16629 * @param {Object} constants and object with default, global properties
16630 */
16631 Edge.prototype.setProperties = function(properties) {
16632 if (!properties) {
16633 return;
16634 }
16635
16636 var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
16637 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor'
16638 ];
16639 util.selectiveDeepExtend(fields, this.options, properties);
16640
16641 if (properties.from !== undefined) {this.fromId = properties.from;}
16642 if (properties.to !== undefined) {this.toId = properties.to;}
16643
16644 if (properties.id !== undefined) {this.id = properties.id;}
16645 if (properties.label !== undefined) {this.label = properties.label;}
16646
16647 if (properties.title !== undefined) {this.title = properties.title;}
16648 if (properties.value !== undefined) {this.value = properties.value;}
16649 if (properties.length !== undefined) {this.physics.springLength = properties.length;}
16650
16651 if (properties.color !== undefined) {
16652 this.options.inheritColor = false;
16653 if (util.isString(properties.color)) {
16654 this.options.color.color = properties.color;
16655 this.options.color.highlight = properties.color;
16656 }
16657 else {
16658 if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
16659 if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
16660 if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
16661 }
16662 }
16663
16664 // A node is connected when it has a from and to node.
16665 this.connect();
16666
16667 this.widthFixed = this.widthFixed || (properties.width !== undefined);
16668 this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
16669
16670 this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
16671
16672 // set draw method based on style
16673 switch (this.options.style) {
16674 case 'line': this.draw = this._drawLine; break;
16675 case 'arrow': this.draw = this._drawArrow; break;
16676 case 'arrow-center': this.draw = this._drawArrowCenter; break;
16677 case 'dash-line': this.draw = this._drawDashLine; break;
16678 default: this.draw = this._drawLine; break;
16679 }
16680 };
16681
16682 /**
16683 * Connect an edge to its nodes
16684 */
16685 Edge.prototype.connect = function () {
16686 this.disconnect();
16687
16688 this.from = this.network.nodes[this.fromId] || null;
16689 this.to = this.network.nodes[this.toId] || null;
16690 this.connected = (this.from && this.to);
16691
16692 if (this.connected) {
16693 this.from.attachEdge(this);
16694 this.to.attachEdge(this);
16695 }
16696 else {
16697 if (this.from) {
16698 this.from.detachEdge(this);
16699 }
16700 if (this.to) {
16701 this.to.detachEdge(this);
16702 }
16703 }
16704 };
16705
16706 /**
16707 * Disconnect an edge from its nodes
16708 */
16709 Edge.prototype.disconnect = function () {
16710 if (this.from) {
16711 this.from.detachEdge(this);
16712 this.from = null;
16713 }
16714 if (this.to) {
16715 this.to.detachEdge(this);
16716 this.to = null;
16717 }
16718
16719 this.connected = false;
16720 };
16721
16722 /**
16723 * get the title of this edge.
16724 * @return {string} title The title of the edge, or undefined when no title
16725 * has been set.
16726 */
16727 Edge.prototype.getTitle = function() {
16728 return typeof this.title === "function" ? this.title() : this.title;
16729 };
16730
16731
16732 /**
16733 * Retrieve the value of the edge. Can be undefined
16734 * @return {Number} value
16735 */
16736 Edge.prototype.getValue = function() {
16737 return this.value;
16738 };
16739
16740 /**
16741 * Adjust the value range of the edge. The edge will adjust it's width
16742 * based on its value.
16743 * @param {Number} min
16744 * @param {Number} max
16745 */
16746 Edge.prototype.setValueRange = function(min, max) {
16747 if (!this.widthFixed && this.value !== undefined) {
16748 var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
16749 this.options.width= (this.value - min) * scale + this.options.widthMin;
16750 this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
16751 }
16752 };
16753
16754 /**
16755 * Redraw a edge
16756 * Draw this edge in the given canvas
16757 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
16758 * @param {CanvasRenderingContext2D} ctx
16759 */
16760 Edge.prototype.draw = function(ctx) {
16761 throw "Method draw not initialized in edge";
16762 };
16763
16764 /**
16765 * Check if this object is overlapping with the provided object
16766 * @param {Object} obj an object with parameters left, top
16767 * @return {boolean} True if location is located on the edge
16768 */
16769 Edge.prototype.isOverlappingWith = function(obj) {
16770 if (this.connected) {
16771 var distMax = 10;
16772 var xFrom = this.from.x;
16773 var yFrom = this.from.y;
16774 var xTo = this.to.x;
16775 var yTo = this.to.y;
16776 var xObj = obj.left;
16777 var yObj = obj.top;
16778
16779 var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
16780
16781 return (dist < distMax);
16782 }
16783 else {
16784 return false
16785 }
16786 };
16787
16788 Edge.prototype._getColor = function() {
16789 var colorObj = this.options.color;
16790 if (this.options.inheritColor == "to") {
16791 colorObj = {
16792 highlight: this.to.options.color.highlight.border,
16793 hover: this.to.options.color.hover.border,
16794 color: this.to.options.color.border
16795 };
16796 }
16797 else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
16798 colorObj = {
16799 highlight: this.from.options.color.highlight.border,
16800 hover: this.from.options.color.hover.border,
16801 color: this.from.options.color.border
16802 };
16803 }
16804
16805 if (this.selected == true) {return colorObj.highlight;}
16806 else if (this.hover == true) {return colorObj.hover;}
16807 else {return colorObj.color;}
16808 }
16809
16810
16811 /**
16812 * Redraw a edge as a line
16813 * Draw this edge in the given canvas
16814 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
16815 * @param {CanvasRenderingContext2D} ctx
16816 * @private
16817 */
16818 Edge.prototype._drawLine = function(ctx) {
16819 // set style
16820 ctx.strokeStyle = this._getColor();
16821 ctx.lineWidth = this._getLineWidth();
16822
16823 if (this.from != this.to) {
16824 // draw line
16825 var via = this._line(ctx);
16826
16827 // draw label
16828 var point;
16829 if (this.label) {
16830 if (this.options.smoothCurves.enabled == true && via != null) {
16831 var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
16832 var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
16833 point = {x:midpointX, y:midpointY};
16834 }
16835 else {
16836 point = this._pointOnLine(0.5);
16837 }
16838 this._label(ctx, this.label, point.x, point.y);
16839 }
16840 }
16841 else {
16842 var x, y;
16843 var radius = this.physics.springLength / 4;
16844 var node = this.from;
16845 if (!node.width) {
16846 node.resize(ctx);
16847 }
16848 if (node.width > node.height) {
16849 x = node.x + node.width / 2;
16850 y = node.y - radius;
16851 }
16852 else {
16853 x = node.x + radius;
16854 y = node.y - node.height / 2;
16855 }
16856 this._circle(ctx, x, y, radius);
16857 point = this._pointOnCircle(x, y, radius, 0.5);
16858 this._label(ctx, this.label, point.x, point.y);
16859 }
16860 };
16861
16862 /**
16863 * Get the line width of the edge. Depends on width and whether one of the
16864 * connected nodes is selected.
16865 * @return {Number} width
16866 * @private
16867 */
16868 Edge.prototype._getLineWidth = function() {
16869 if (this.selected == true) {
16870 return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
16871 }
16872 else {
16873 if (this.hover == true) {
16874 return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
16875 }
16876 else {
16877 return Math.max(this.options.width, 0.3*this.networkScaleInv);
16878 }
16879 }
16880 };
16881
16882 Edge.prototype._getViaCoordinates = function () {
16883 var xVia = null;
16884 var yVia = null;
16885 var factor = this.options.smoothCurves.roundness;
16886 var type = this.options.smoothCurves.type;
16887
16888 var dx = Math.abs(this.from.x - this.to.x);
16889 var dy = Math.abs(this.from.y - this.to.y);
16890 if (type == 'discrete' || type == 'diagonalCross') {
16891 if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
16892 if (this.from.y > this.to.y) {
16893 if (this.from.x < this.to.x) {
16894 xVia = this.from.x + factor * dy;
16895 yVia = this.from.y - factor * dy;
16896 }
16897 else if (this.from.x > this.to.x) {
16898 xVia = this.from.x - factor * dy;
16899 yVia = this.from.y - factor * dy;
16900 }
16901 }
16902 else if (this.from.y < this.to.y) {
16903 if (this.from.x < this.to.x) {
16904 xVia = this.from.x + factor * dy;
16905 yVia = this.from.y + factor * dy;
16906 }
16907 else if (this.from.x > this.to.x) {
16908 xVia = this.from.x - factor * dy;
16909 yVia = this.from.y + factor * dy;
16910 }
16911 }
16912 if (type == "discrete") {
16913 xVia = dx < factor * dy ? this.from.x : xVia;
16914 }
16915 }
16916 else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
16917 if (this.from.y > this.to.y) {
16918 if (this.from.x < this.to.x) {
16919 xVia = this.from.x + factor * dx;
16920 yVia = this.from.y - factor * dx;
16921 }
16922 else if (this.from.x > this.to.x) {
16923 xVia = this.from.x - factor * dx;
16924 yVia = this.from.y - factor * dx;
16925 }
16926 }
16927 else if (this.from.y < this.to.y) {
16928 if (this.from.x < this.to.x) {
16929 xVia = this.from.x + factor * dx;
16930 yVia = this.from.y + factor * dx;
16931 }
16932 else if (this.from.x > this.to.x) {
16933 xVia = this.from.x - factor * dx;
16934 yVia = this.from.y + factor * dx;
16935 }
16936 }
16937 if (type == "discrete") {
16938 yVia = dy < factor * dx ? this.from.y : yVia;
16939 }
16940 }
16941 }
16942 else if (type == "straightCross") {
16943 if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
16944 xVia = this.from.x;
16945 if (this.from.y < this.to.y) {
16946 yVia = this.to.y - (1-factor) * dy;
16947 }
16948 else {
16949 yVia = this.to.y + (1-factor) * dy;
16950 }
16951 }
16952 else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
16953 if (this.from.x < this.to.x) {
16954 xVia = this.to.x - (1-factor) * dx;
16955 }
16956 else {
16957 xVia = this.to.x + (1-factor) * dx;
16958 }
16959 yVia = this.from.y;
16960 }
16961 }
16962 else if (type == 'horizontal') {
16963 if (this.from.x < this.to.x) {
16964 xVia = this.to.x - (1-factor) * dx;
16965 }
16966 else {
16967 xVia = this.to.x + (1-factor) * dx;
16968 }
16969 yVia = this.from.y;
16970 }
16971 else if (type == 'vertical') {
16972 xVia = this.from.x;
16973 if (this.from.y < this.to.y) {
16974 yVia = this.to.y - (1-factor) * dy;
16975 }
16976 else {
16977 yVia = this.to.y + (1-factor) * dy;
16978 }
16979 }
16980 else { // continuous
16981 if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
16982 if (this.from.y > this.to.y) {
16983 if (this.from.x < this.to.x) {
16984 // console.log(1)
16985 xVia = this.from.x + factor * dy;
16986 yVia = this.from.y - factor * dy;
16987 xVia = this.to.x < xVia ? this.to.x : xVia;
16988 }
16989 else if (this.from.x > this.to.x) {
16990 // console.log(2)
16991 xVia = this.from.x - factor * dy;
16992 yVia = this.from.y - factor * dy;
16993 xVia = this.to.x > xVia ? this.to.x :xVia;
16994 }
16995 }
16996 else if (this.from.y < this.to.y) {
16997 if (this.from.x < this.to.x) {
16998 // console.log(3)
16999 xVia = this.from.x + factor * dy;
17000 yVia = this.from.y + factor * dy;
17001 xVia = this.to.x < xVia ? this.to.x : xVia;
17002 }
17003 else if (this.from.x > this.to.x) {
17004 // console.log(4, this.from.x, this.to.x)
17005 xVia = this.from.x - factor * dy;
17006 yVia = this.from.y + factor * dy;
17007 xVia = this.to.x > xVia ? this.to.x : xVia;
17008 }
17009 }
17010 }
17011 else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
17012 if (this.from.y > this.to.y) {
17013 if (this.from.x < this.to.x) {
17014 // console.log(5)
17015 xVia = this.from.x + factor * dx;
17016 yVia = this.from.y - factor * dx;
17017 yVia = this.to.y > yVia ? this.to.y : yVia;
17018 }
17019 else if (this.from.x > this.to.x) {
17020 // console.log(6)
17021 xVia = this.from.x - factor * dx;
17022 yVia = this.from.y - factor * dx;
17023 yVia = this.to.y > yVia ? this.to.y : yVia;
17024 }
17025 }
17026 else if (this.from.y < this.to.y) {
17027 if (this.from.x < this.to.x) {
17028 // console.log(7)
17029 xVia = this.from.x + factor * dx;
17030 yVia = this.from.y + factor * dx;
17031 yVia = this.to.y < yVia ? this.to.y : yVia;
17032 }
17033 else if (this.from.x > this.to.x) {
17034 // console.log(8)
17035 xVia = this.from.x - factor * dx;
17036 yVia = this.from.y + factor * dx;
17037 yVia = this.to.y < yVia ? this.to.y : yVia;
17038 }
17039 }
17040 }
17041 }
17042
17043
17044 return {x:xVia, y:yVia};
17045 }
17046
17047 /**
17048 * Draw a line between two nodes
17049 * @param {CanvasRenderingContext2D} ctx
17050 * @private
17051 */
17052 Edge.prototype._line = function (ctx) {
17053 // draw a straight line
17054 ctx.beginPath();
17055 ctx.moveTo(this.from.x, this.from.y);
17056 if (this.options.smoothCurves.enabled == true) {
17057 if (this.options.smoothCurves.dynamic == false) {
17058 var via = this._getViaCoordinates();
17059 if (via.x == null) {
17060 ctx.lineTo(this.to.x, this.to.y);
17061 ctx.stroke();
17062 return null;
17063 }
17064 else {
17065 // this.via.x = via.x;
17066 // this.via.y = via.y;
17067 ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
17068 ctx.stroke();
17069 return via;
17070 }
17071 }
17072 else {
17073 ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
17074 ctx.stroke();
17075 return this.via;
17076 }
17077 }
17078 else {
17079 ctx.lineTo(this.to.x, this.to.y);
17080 ctx.stroke();
17081 return null;
17082 }
17083 };
17084
17085 /**
17086 * Draw a line from a node to itself, a circle
17087 * @param {CanvasRenderingContext2D} ctx
17088 * @param {Number} x
17089 * @param {Number} y
17090 * @param {Number} radius
17091 * @private
17092 */
17093 Edge.prototype._circle = function (ctx, x, y, radius) {
17094 // draw a circle
17095 ctx.beginPath();
17096 ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
17097 ctx.stroke();
17098 };
17099
17100 /**
17101 * Draw label with white background and with the middle at (x, y)
17102 * @param {CanvasRenderingContext2D} ctx
17103 * @param {String} text
17104 * @param {Number} x
17105 * @param {Number} y
17106 * @private
17107 */
17108 Edge.prototype._label = function (ctx, text, x, y) {
17109 if (text) {
17110 // TODO: cache the calculated size
17111 ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
17112 this.options.fontSize + "px " + this.options.fontFace;
17113
17114
17115 var lines = String(text).split('\n');
17116 var lineCount = lines.length;
17117 var fontSize = (Number(this.options.fontSize) + 4);
17118 var yLine = y + (1 - lineCount) / 2 * fontSize;
17119
17120 var width = ctx.measureText(lines[0]).width;
17121 for (var i = 1; i < lineCount; i++) {
17122 var lineWidth = ctx.measureText(lines[i]).width;
17123 width = lineWidth > width ? lineWidth : width;
17124 }
17125 var height = this.options.fontSize * lineCount;
17126 var left = x - width / 2;
17127 var top = y - height / 2;
17128
17129 this.labelDimensions = {top:top,left:left,width:width,height:height};
17130
17131 if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
17132 ctx.fillStyle = this.options.fontFill;
17133 ctx.fillRect(left, top, width, height);
17134 }
17135
17136 // draw text
17137 ctx.fillStyle = this.options.fontColor || "black";
17138 ctx.textAlign = "center";
17139 ctx.textBaseline = "middle";
17140
17141 for (var i = 0; i < lineCount; i++) {
17142 ctx.fillText(lines[i], x, yLine);
17143 yLine += fontSize;
17144 }
17145 }
17146 };
17147
17148 /**
17149 * Redraw a edge as a dashed line
17150 * Draw this edge in the given canvas
17151 * @author David Jordan
17152 * @date 2012-08-08
17153 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
17154 * @param {CanvasRenderingContext2D} ctx
17155 * @private
17156 */
17157 Edge.prototype._drawDashLine = function(ctx) {
17158 // set style
17159 if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight;}
17160 else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover;}
17161 else {ctx.strokeStyle = this.options.color.color;}
17162
17163 ctx.lineWidth = this._getLineWidth();
17164
17165 var via = null;
17166 // only firefox and chrome support this method, else we use the legacy one.
17167 if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
17168 // configure the dash pattern
17169 var pattern = [0];
17170 if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
17171 pattern = [this.options.dash.length,this.options.dash.gap];
17172 }
17173 else {
17174 pattern = [5,5];
17175 }
17176
17177 // set dash settings for chrome or firefox
17178 if (typeof ctx.setLineDash !== 'undefined') { //Chrome
17179 ctx.setLineDash(pattern);
17180 ctx.lineDashOffset = 0;
17181
17182 } else { //Firefox
17183 ctx.mozDash = pattern;
17184 ctx.mozDashOffset = 0;
17185 }
17186
17187 // draw the line
17188 via = this._line(ctx);
17189
17190 // restore the dash settings.
17191 if (typeof ctx.setLineDash !== 'undefined') { //Chrome
17192 ctx.setLineDash([0]);
17193 ctx.lineDashOffset = 0;
17194
17195 } else { //Firefox
17196 ctx.mozDash = [0];
17197 ctx.mozDashOffset = 0;
17198 }
17199 }
17200 else { // unsupporting smooth lines
17201 // draw dashed line
17202 ctx.beginPath();
17203 ctx.lineCap = 'round';
17204 if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
17205 {
17206 ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
17207 [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
17208 }
17209 else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
17210 {
17211 ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
17212 [this.options.dash.length,this.options.dash.gap]);
17213 }
17214 else //If all else fails draw a line
17215 {
17216 ctx.moveTo(this.from.x, this.from.y);
17217 ctx.lineTo(this.to.x, this.to.y);
17218 }
17219 ctx.stroke();
17220 }
17221
17222 // draw label
17223 if (this.label) {
17224 var point;
17225 if (this.options.smoothCurves.enabled == true && via != null) {
17226 var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
17227 var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
17228 point = {x:midpointX, y:midpointY};
17229 }
17230 else {
17231 point = this._pointOnLine(0.5);
17232 }
17233 this._label(ctx, this.label, point.x, point.y);
17234 }
17235 };
17236
17237 /**
17238 * Get a point on a line
17239 * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
17240 * @return {Object} point
17241 * @private
17242 */
17243 Edge.prototype._pointOnLine = function (percentage) {
17244 return {
17245 x: (1 - percentage) * this.from.x + percentage * this.to.x,
17246 y: (1 - percentage) * this.from.y + percentage * this.to.y
17247 }
17248 };
17249
17250 /**
17251 * Get a point on a circle
17252 * @param {Number} x
17253 * @param {Number} y
17254 * @param {Number} radius
17255 * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
17256 * @return {Object} point
17257 * @private
17258 */
17259 Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
17260 var angle = (percentage - 3/8) * 2 * Math.PI;
17261 return {
17262 x: x + radius * Math.cos(angle),
17263 y: y - radius * Math.sin(angle)
17264 }
17265 };
17266
17267 /**
17268 * Redraw a edge as a line with an arrow halfway the line
17269 * Draw this edge in the given canvas
17270 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
17271 * @param {CanvasRenderingContext2D} ctx
17272 * @private
17273 */
17274 Edge.prototype._drawArrowCenter = function(ctx) {
17275 var point;
17276 // set style
17277 if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;}
17278 else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;}
17279 else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;}
17280 ctx.lineWidth = this._getLineWidth();
17281
17282 if (this.from != this.to) {
17283 // draw line
17284 var via = this._line(ctx);
17285
17286 var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
17287 var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
17288 // draw an arrow halfway the line
17289 if (this.options.smoothCurves.enabled == true && via != null) {
17290 var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
17291 var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
17292 point = {x:midpointX, y:midpointY};
17293 }
17294 else {
17295 point = this._pointOnLine(0.5);
17296 }
17297
17298 ctx.arrow(point.x, point.y, angle, length);
17299 ctx.fill();
17300 ctx.stroke();
17301
17302 // draw label
17303 if (this.label) {
17304 this._label(ctx, this.label, point.x, point.y);
17305 }
17306 }
17307 else {
17308 // draw circle
17309 var x, y;
17310 var radius = 0.25 * Math.max(100,this.physics.springLength);
17311 var node = this.from;
17312 if (!node.width) {
17313 node.resize(ctx);
17314 }
17315 if (node.width > node.height) {
17316 x = node.x + node.width * 0.5;
17317 y = node.y - radius;
17318 }
17319 else {
17320 x = node.x + radius;
17321 y = node.y - node.height * 0.5;
17322 }
17323 this._circle(ctx, x, y, radius);
17324
17325 // draw all arrows
17326 var angle = 0.2 * Math.PI;
17327 var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
17328 point = this._pointOnCircle(x, y, radius, 0.5);
17329 ctx.arrow(point.x, point.y, angle, length);
17330 ctx.fill();
17331 ctx.stroke();
17332
17333 // draw label
17334 if (this.label) {
17335 point = this._pointOnCircle(x, y, radius, 0.5);
17336 this._label(ctx, this.label, point.x, point.y);
17337 }
17338 }
17339 };
17340
17341
17342
17343 /**
17344 * Redraw a edge as a line with an arrow
17345 * Draw this edge in the given canvas
17346 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
17347 * @param {CanvasRenderingContext2D} ctx
17348 * @private
17349 */
17350 Edge.prototype._drawArrow = function(ctx) {
17351 // set style
17352 if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;}
17353 else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;}
17354 else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;}
17355
17356 ctx.lineWidth = this._getLineWidth();
17357
17358 var angle, length;
17359 //draw a line
17360 if (this.from != this.to) {
17361 angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
17362 var dx = (this.to.x - this.from.x);
17363 var dy = (this.to.y - this.from.y);
17364 var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
17365
17366 var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
17367 var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
17368 var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
17369 var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
17370
17371 var via;
17372 if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
17373 via = this.via;
17374 }
17375 else if (this.options.smoothCurves.enabled == true) {
17376 via = this._getViaCoordinates();
17377 }
17378
17379 if (this.options.smoothCurves.enabled == true && via.x != null) {
17380 angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
17381 dx = (this.to.x - via.x);
17382 dy = (this.to.y - via.y);
17383 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
17384 }
17385 var toBorderDist = this.to.distanceToBorder(ctx, angle);
17386 var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
17387
17388 var xTo,yTo;
17389 if (this.options.smoothCurves.enabled == true && via.x != null) {
17390 xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
17391 yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
17392 }
17393 else {
17394 xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
17395 yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
17396 }
17397
17398 ctx.beginPath();
17399 ctx.moveTo(xFrom,yFrom);
17400 if (this.options.smoothCurves.enabled == true && via.x != null) {
17401 ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
17402 }
17403 else {
17404 ctx.lineTo(xTo, yTo);
17405 }
17406 ctx.stroke();
17407
17408 // draw arrow at the end of the line
17409 length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
17410 ctx.arrow(xTo, yTo, angle, length);
17411 ctx.fill();
17412 ctx.stroke();
17413
17414 // draw label
17415 if (this.label) {
17416 var point;
17417 if (this.options.smoothCurves.enabled == true && via != null) {
17418 var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
17419 var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
17420 point = {x:midpointX, y:midpointY};
17421 }
17422 else {
17423 point = this._pointOnLine(0.5);
17424 }
17425 this._label(ctx, this.label, point.x, point.y);
17426 }
17427 }
17428 else {
17429 // draw circle
17430 var node = this.from;
17431 var x, y, arrow;
17432 var radius = 0.25 * Math.max(100,this.physics.springLength);
17433 if (!node.width) {
17434 node.resize(ctx);
17435 }
17436 if (node.width > node.height) {
17437 x = node.x + node.width * 0.5;
17438 y = node.y - radius;
17439 arrow = {
17440 x: x,
17441 y: node.y,
17442 angle: 0.9 * Math.PI
17443 };
17444 }
17445 else {
17446 x = node.x + radius;
17447 y = node.y - node.height * 0.5;
17448 arrow = {
17449 x: node.x,
17450 y: y,
17451 angle: 0.6 * Math.PI
17452 };
17453 }
17454 ctx.beginPath();
17455 // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
17456 ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
17457 ctx.stroke();
17458
17459 // draw all arrows
17460 var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
17461 ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
17462 ctx.fill();
17463 ctx.stroke();
17464
17465 // draw label
17466 if (this.label) {
17467 point = this._pointOnCircle(x, y, radius, 0.5);
17468 this._label(ctx, this.label, point.x, point.y);
17469 }
17470 }
17471 };
17472
17473
17474
17475 /**
17476 * Calculate the distance between a point (x3,y3) and a line segment from
17477 * (x1,y1) to (x2,y2).
17478 * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
17479 * @param {number} x1
17480 * @param {number} y1
17481 * @param {number} x2
17482 * @param {number} y2
17483 * @param {number} x3
17484 * @param {number} y3
17485 * @private
17486 */
17487 Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
17488 var returnValue = 0;
17489 if (this.from != this.to) {
17490 if (this.options.smoothCurves.enabled == true) {
17491 var xVia, yVia;
17492 if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
17493 xVia = this.via.x;
17494 yVia = this.via.y;
17495 }
17496 else {
17497 var via = this._getViaCoordinates();
17498 xVia = via.x;
17499 yVia = via.y;
17500 }
17501 var minDistance = 1e9;
17502 var distance;
17503 var i,t,x,y, lastX, lastY;
17504 for (i = 0; i < 10; i++) {
17505 t = 0.1*i;
17506 x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
17507 y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
17508 if (i > 0) {
17509 distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
17510 minDistance = distance < minDistance ? distance : minDistance;
17511 }
17512 lastX = x; lastY = y;
17513 }
17514 returnValue = minDistance;
17515 }
17516 else {
17517 returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
17518 }
17519 }
17520 else {
17521 var x, y, dx, dy;
17522 var radius = 0.25 * this.physics.springLength;
17523 var node = this.from;
17524 if (node.width > node.height) {
17525 x = node.x + 0.5 * node.width;
17526 y = node.y - radius;
17527 }
17528 else {
17529 x = node.x + radius;
17530 y = node.y - 0.5 * node.height;
17531 }
17532 dx = x - x3;
17533 dy = y - y3;
17534 returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
17535 }
17536
17537 if (this.labelDimensions.left < x3 &&
17538 this.labelDimensions.left + this.labelDimensions.width > x3 &&
17539 this.labelDimensions.top < y3 &&
17540 this.labelDimensions.top + this.labelDimensions.height > y3) {
17541 return 0;
17542 }
17543 else {
17544 return returnValue;
17545 }
17546 };
17547
17548 Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
17549 var px = x2-x1,
17550 py = y2-y1,
17551 something = px*px + py*py,
17552 u = ((x3 - x1) * px + (y3 - y1) * py) / something;
17553
17554 if (u > 1) {
17555 u = 1;
17556 }
17557 else if (u < 0) {
17558 u = 0;
17559 }
17560
17561 var x = x1 + u * px,
17562 y = y1 + u * py,
17563 dx = x - x3,
17564 dy = y - y3;
17565
17566 //# Note: If the actual distance does not matter,
17567 //# if you only want to compare what this function
17568 //# returns to other results of this function, you
17569 //# can just return the squared distance instead
17570 //# (i.e. remove the sqrt) to gain a little performance
17571
17572 return Math.sqrt(dx*dx + dy*dy);
17573 }
17574
17575 /**
17576 * This allows the zoom level of the network to influence the rendering
17577 *
17578 * @param scale
17579 */
17580 Edge.prototype.setScale = function(scale) {
17581 this.networkScaleInv = 1.0/scale;
17582 };
17583
17584
17585 Edge.prototype.select = function() {
17586 this.selected = true;
17587 };
17588
17589 Edge.prototype.unselect = function() {
17590 this.selected = false;
17591 };
17592
17593 Edge.prototype.positionBezierNode = function() {
17594 if (this.via !== null && this.from !== null && this.to !== null) {
17595 this.via.x = 0.5 * (this.from.x + this.to.x);
17596 this.via.y = 0.5 * (this.from.y + this.to.y);
17597 }
17598 };
17599
17600 /**
17601 * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
17602 * @param ctx
17603 */
17604 Edge.prototype._drawControlNodes = function(ctx) {
17605 if (this.controlNodesEnabled == true) {
17606 if (this.controlNodes.from === null && this.controlNodes.to === null) {
17607 var nodeIdFrom = "edgeIdFrom:".concat(this.id);
17608 var nodeIdTo = "edgeIdTo:".concat(this.id);
17609 var constants = {
17610 nodes:{group:'', radius:8},
17611 physics:{damping:0},
17612 clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
17613 };
17614 this.controlNodes.from = new Node(
17615 {id:nodeIdFrom,
17616 shape:'dot',
17617 color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
17618 },{},{},constants);
17619 this.controlNodes.to = new Node(
17620 {id:nodeIdTo,
17621 shape:'dot',
17622 color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
17623 },{},{},constants);
17624 }
17625
17626 if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
17627 this.controlNodes.positions = this.getControlNodePositions(ctx);
17628 this.controlNodes.from.x = this.controlNodes.positions.from.x;
17629 this.controlNodes.from.y = this.controlNodes.positions.from.y;
17630 this.controlNodes.to.x = this.controlNodes.positions.to.x;
17631 this.controlNodes.to.y = this.controlNodes.positions.to.y;
17632 }
17633
17634 this.controlNodes.from.draw(ctx);
17635 this.controlNodes.to.draw(ctx);
17636 }
17637 else {
17638 this.controlNodes = {from:null, to:null, positions:{}};
17639 }
17640 };
17641
17642 /**
17643 * Enable control nodes.
17644 * @private
17645 */
17646 Edge.prototype._enableControlNodes = function() {
17647 this.controlNodesEnabled = true;
17648 };
17649
17650 /**
17651 * disable control nodes
17652 * @private
17653 */
17654 Edge.prototype._disableControlNodes = function() {
17655 this.controlNodesEnabled = false;
17656 };
17657
17658 /**
17659 * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
17660 * @param x
17661 * @param y
17662 * @returns {null}
17663 * @private
17664 */
17665 Edge.prototype._getSelectedControlNode = function(x,y) {
17666 var positions = this.controlNodes.positions;
17667 var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
17668 var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
17669
17670 if (fromDistance < 15) {
17671 this.connectedNode = this.from;
17672 this.from = this.controlNodes.from;
17673 return this.controlNodes.from;
17674 }
17675 else if (toDistance < 15) {
17676 this.connectedNode = this.to;
17677 this.to = this.controlNodes.to;
17678 return this.controlNodes.to;
17679 }
17680 else {
17681 return null;
17682 }
17683 };
17684
17685
17686 /**
17687 * this resets the control nodes to their original position.
17688 * @private
17689 */
17690 Edge.prototype._restoreControlNodes = function() {
17691 if (this.controlNodes.from.selected == true) {
17692 this.from = this.connectedNode;
17693 this.connectedNode = null;
17694 this.controlNodes.from.unselect();
17695 }
17696 if (this.controlNodes.to.selected == true) {
17697 this.to = this.connectedNode;
17698 this.connectedNode = null;
17699 this.controlNodes.to.unselect();
17700 }
17701 };
17702
17703 /**
17704 * this calculates the position of the control nodes on the edges of the parent nodes.
17705 *
17706 * @param ctx
17707 * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
17708 */
17709 Edge.prototype.getControlNodePositions = function(ctx) {
17710 var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
17711 var dx = (this.to.x - this.from.x);
17712 var dy = (this.to.y - this.from.y);
17713 var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
17714 var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
17715 var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
17716 var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
17717 var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
17718
17719 var via;
17720 if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
17721 via = this.via;
17722 }
17723 else if (this.options.smoothCurves.enabled == true) {
17724 via = this._getViaCoordinates();
17725 }
17726
17727 if (this.options.smoothCurves.enabled == true && via.x != null) {
17728 angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
17729 dx = (this.to.x - via.x);
17730 dy = (this.to.y - via.y);
17731 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
17732 }
17733 var toBorderDist = this.to.distanceToBorder(ctx, angle);
17734 var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
17735
17736 var xTo,yTo;
17737 if (this.options.smoothCurves.enabled == true && via.x != null) {
17738 xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
17739 yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
17740 }
17741 else {
17742 xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
17743 yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
17744 }
17745
17746 return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
17747 };
17748
17749 module.exports = Edge;
17750
17751/***/ },
17752/* 35 */
17753/***/ function(module, exports, __webpack_require__) {
17754
17755 var util = __webpack_require__(1);
17756
17757 /**
17758 * @class Groups
17759 * This class can store groups and properties specific for groups.
17760 */
17761 function Groups() {
17762 this.clear();
17763 this.defaultIndex = 0;
17764 }
17765
17766
17767 /**
17768 * default constants for group colors
17769 */
17770 Groups.DEFAULT = [
17771 {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
17772 {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
17773 {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
17774 {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
17775 {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
17776 {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
17777 {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
17778 {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
17779 {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
17780 {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
17781 ];
17782
17783
17784 /**
17785 * Clear all groups
17786 */
17787 Groups.prototype.clear = function () {
17788 this.groups = {};
17789 this.groups.length = function()
17790 {
17791 var i = 0;
17792 for ( var p in this ) {
17793 if (this.hasOwnProperty(p)) {
17794 i++;
17795 }
17796 }
17797 return i;
17798 }
17799 };
17800
17801
17802 /**
17803 * get group properties of a groupname. If groupname is not found, a new group
17804 * is added.
17805 * @param {*} groupname Can be a number, string, Date, etc.
17806 * @return {Object} group The created group, containing all group properties
17807 */
17808 Groups.prototype.get = function (groupname) {
17809 var group = this.groups[groupname];
17810 if (group == undefined) {
17811 // create new group
17812 var index = this.defaultIndex % Groups.DEFAULT.length;
17813 this.defaultIndex++;
17814 group = {};
17815 group.color = Groups.DEFAULT[index];
17816 this.groups[groupname] = group;
17817 }
17818
17819 return group;
17820 };
17821
17822 /**
17823 * Add a custom group style
17824 * @param {String} groupname
17825 * @param {Object} style An object containing borderColor,
17826 * backgroundColor, etc.
17827 * @return {Object} group The created group object
17828 */
17829 Groups.prototype.add = function (groupname, style) {
17830 this.groups[groupname] = style;
17831 if (style.color) {
17832 style.color = util.parseColor(style.color);
17833 }
17834 return style;
17835 };
17836
17837 module.exports = Groups;
17838
17839
17840/***/ },
17841/* 36 */
17842/***/ function(module, exports, __webpack_require__) {
17843
17844 /**
17845 * @class Images
17846 * This class loads images and keeps them stored.
17847 */
17848 function Images() {
17849 this.images = {};
17850
17851 this.callback = undefined;
17852 }
17853
17854 /**
17855 * Set an onload callback function. This will be called each time an image
17856 * is loaded
17857 * @param {function} callback
17858 */
17859 Images.prototype.setOnloadCallback = function(callback) {
17860 this.callback = callback;
17861 };
17862
17863 /**
17864 *
17865 * @param {string} url Url of the image
17866 * @param {string} url Url of an image to use if the url image is not found
17867 * @return {Image} img The image object
17868 */
17869 Images.prototype.load = function(url, brokenUrl) {
17870 var img = this.images[url];
17871 if (img == undefined) {
17872 // create the image
17873 var images = this;
17874 img = new Image();
17875 this.images[url] = img;
17876 img.onload = function() {
17877 if (images.callback) {
17878 images.callback(this);
17879 }
17880 };
17881
17882 img.onerror = function () {
17883 this.src = brokenUrl;
17884 if (images.callback) {
17885 images.callback(this);
17886 }
17887 };
17888
17889 img.src = url;
17890 }
17891
17892 return img;
17893 };
17894
17895 module.exports = Images;
17896
17897
17898/***/ },
17899/* 37 */
17900/***/ function(module, exports, __webpack_require__) {
17901
17902 var util = __webpack_require__(1);
17903
17904 /**
17905 * @class Node
17906 * A node. A node can be connected to other nodes via one or multiple edges.
17907 * @param {object} properties An object containing properties for the node. All
17908 * properties are optional, except for the id.
17909 * {number} id Id of the node. Required
17910 * {string} label Text label for the node
17911 * {number} x Horizontal position of the node
17912 * {number} y Vertical position of the node
17913 * {string} shape Node shape, available:
17914 * "database", "circle", "ellipse",
17915 * "box", "image", "text", "dot",
17916 * "star", "triangle", "triangleDown",
17917 * "square"
17918 * {string} image An image url
17919 * {string} title An title text, can be HTML
17920 * {anytype} group A group name or number
17921 * @param {Network.Images} imagelist A list with images. Only needed
17922 * when the node has an image
17923 * @param {Network.Groups} grouplist A list with groups. Needed for
17924 * retrieving group properties
17925 * @param {Object} constants An object with default values for
17926 * example for the color
17927 *
17928 */
17929 function Node(properties, imagelist, grouplist, networkConstants) {
17930 var constants = util.selectiveBridgeObject(['nodes'],networkConstants);
17931 this.options = constants.nodes;
17932
17933 this.selected = false;
17934 this.hover = false;
17935
17936 this.edges = []; // all edges connected to this node
17937 this.dynamicEdges = [];
17938 this.reroutedEdges = {};
17939
17940 this.fontDrawThreshold = 3;
17941
17942 // set defaults for the properties
17943 this.id = undefined;
17944 this.x = null;
17945 this.y = null;
17946 this.allowedToMoveX = false;
17947 this.allowedToMoveY = false;
17948 this.xFixed = false;
17949 this.yFixed = false;
17950 this.horizontalAlignLeft = true; // these are for the navigation controls
17951 this.verticalAlignTop = true; // these are for the navigation controls
17952 this.baseRadiusValue = networkConstants.nodes.radius;
17953 this.radiusFixed = false;
17954 this.level = -1;
17955 this.preassignedLevel = false;
17956 this.hierarchyEnumerated = false;
17957
17958
17959 this.imagelist = imagelist;
17960 this.grouplist = grouplist;
17961
17962 // physics properties
17963 this.fx = 0.0; // external force x
17964 this.fy = 0.0; // external force y
17965 this.vx = 0.0; // velocity x
17966 this.vy = 0.0; // velocity y
17967 this.damping = networkConstants.physics.damping; // written every time gravity is calculated
17968 this.fixedData = {x:null,y:null};
17969
17970 this.setProperties(properties, constants);
17971
17972 // creating the variables for clustering
17973 this.resetCluster();
17974 this.dynamicEdgesLength = 0;
17975 this.clusterSession = 0;
17976 this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
17977 this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
17978 this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
17979 this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
17980 this.growthIndicator = 0;
17981
17982 // variables to tell the node about the network.
17983 this.networkScaleInv = 1;
17984 this.networkScale = 1;
17985 this.canvasTopLeft = {"x": -300, "y": -300};
17986 this.canvasBottomRight = {"x": 300, "y": 300};
17987 this.parentEdgeId = null;
17988 }
17989
17990 /**
17991 * (re)setting the clustering variables and objects
17992 */
17993 Node.prototype.resetCluster = function() {
17994 // clustering variables
17995 this.formationScale = undefined; // this is used to determine when to open the cluster
17996 this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
17997 this.containedNodes = {};
17998 this.containedEdges = {};
17999 this.clusterSessions = [];
18000 };
18001
18002 /**
18003 * Attach a edge to the node
18004 * @param {Edge} edge
18005 */
18006 Node.prototype.attachEdge = function(edge) {
18007 if (this.edges.indexOf(edge) == -1) {
18008 this.edges.push(edge);
18009 }
18010 if (this.dynamicEdges.indexOf(edge) == -1) {
18011 this.dynamicEdges.push(edge);
18012 }
18013 this.dynamicEdgesLength = this.dynamicEdges.length;
18014 };
18015
18016 /**
18017 * Detach a edge from the node
18018 * @param {Edge} edge
18019 */
18020 Node.prototype.detachEdge = function(edge) {
18021 var index = this.edges.indexOf(edge);
18022 if (index != -1) {
18023 this.edges.splice(index, 1);
18024 }
18025 index = this.dynamicEdges.indexOf(edge);
18026 if (index != -1) {
18027 this.dynamicEdges.splice(index, 1);
18028 }
18029 this.dynamicEdgesLength = this.dynamicEdges.length;
18030 };
18031
18032
18033 /**
18034 * Set or overwrite properties for the node
18035 * @param {Object} properties an object with properties
18036 * @param {Object} constants and object with default, global properties
18037 */
18038 Node.prototype.setProperties = function(properties, constants) {
18039 if (!properties) {
18040 return;
18041 }
18042
18043 var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor',
18044 'fontSize','fontFace','fontFill','group','mass'
18045 ];
18046 util.selectiveDeepExtend(fields, this.options, properties);
18047
18048 this.originalLabel = undefined;
18049 // basic properties
18050 if (properties.id !== undefined) {this.id = properties.id;}
18051 if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
18052 if (properties.title !== undefined) {this.title = properties.title;}
18053 if (properties.x !== undefined) {this.x = properties.x;}
18054 if (properties.y !== undefined) {this.y = properties.y;}
18055 if (properties.value !== undefined) {this.value = properties.value;}
18056 if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
18057
18058 // navigation controls properties
18059 if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
18060 if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
18061 if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
18062
18063 if (this.id === undefined) {
18064 throw "Node must have an id";
18065 }
18066
18067 // copy group properties
18068 if (typeof this.options.group === 'number' || (typeof this.options.group === 'string' && this.options.group != '')) {
18069 var groupObj = this.grouplist.get(this.options.group);
18070 for (var prop in groupObj) {
18071 if (groupObj.hasOwnProperty(prop)) {
18072 this.options[prop] = groupObj[prop];
18073 }
18074 }
18075 }
18076
18077
18078 // individual shape properties
18079 if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;}
18080 if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);}
18081
18082 if (this.options.image!== undefined && this.options.image!= "") {
18083 if (this.imagelist) {
18084 this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage);
18085 }
18086 else {
18087 throw "No imagelist provided";
18088 }
18089 }
18090
18091 if (properties.allowedToMoveX !== undefined) {
18092 this.xFixed = !properties.allowedToMoveX;
18093 this.allowedToMoveX = properties.allowedToMoveX;
18094 }
18095 else if (properties.x !== undefined && this.allowedToMoveX == false) {
18096 this.xFixed = true;
18097 }
18098
18099
18100 if (properties.allowedToMoveY !== undefined) {
18101 this.yFixed = !properties.allowedToMoveY;
18102 this.allowedToMoveY = properties.allowedToMoveY;
18103 }
18104 else if (properties.y !== undefined && this.allowedToMoveY == false) {
18105 this.yFixed = true;
18106 }
18107
18108 this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
18109
18110 if (this.options.shape == 'image') {
18111 this.options.radiusMin = constants.nodes.widthMin;
18112 this.options.radiusMax = constants.nodes.widthMax;
18113 }
18114
18115
18116
18117 // choose draw method depending on the shape
18118 switch (this.options.shape) {
18119 case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
18120 case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
18121 case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
18122 case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
18123 // TODO: add diamond shape
18124 case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
18125 case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
18126 case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
18127 case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
18128 case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
18129 case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
18130 case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
18131 default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
18132 }
18133 // reset the size of the node, this can be changed
18134 this._reset();
18135
18136 };
18137
18138 /**
18139 * select this node
18140 */
18141 Node.prototype.select = function() {
18142 this.selected = true;
18143 this._reset();
18144 };
18145
18146 /**
18147 * unselect this node
18148 */
18149 Node.prototype.unselect = function() {
18150 this.selected = false;
18151 this._reset();
18152 };
18153
18154
18155 /**
18156 * Reset the calculated size of the node, forces it to recalculate its size
18157 */
18158 Node.prototype.clearSizeCache = function() {
18159 this._reset();
18160 };
18161
18162 /**
18163 * Reset the calculated size of the node, forces it to recalculate its size
18164 * @private
18165 */
18166 Node.prototype._reset = function() {
18167 this.width = undefined;
18168 this.height = undefined;
18169 };
18170
18171 /**
18172 * get the title of this node.
18173 * @return {string} title The title of the node, or undefined when no title
18174 * has been set.
18175 */
18176 Node.prototype.getTitle = function() {
18177 return typeof this.title === "function" ? this.title() : this.title;
18178 };
18179
18180 /**
18181 * Calculate the distance to the border of the Node
18182 * @param {CanvasRenderingContext2D} ctx
18183 * @param {Number} angle Angle in radians
18184 * @returns {number} distance Distance to the border in pixels
18185 */
18186 Node.prototype.distanceToBorder = function (ctx, angle) {
18187 var borderWidth = 1;
18188
18189 if (!this.width) {
18190 this.resize(ctx);
18191 }
18192
18193 switch (this.options.shape) {
18194 case 'circle':
18195 case 'dot':
18196 return this.options.radius+ borderWidth;
18197
18198 case 'ellipse':
18199 var a = this.width / 2;
18200 var b = this.height / 2;
18201 var w = (Math.sin(angle) * a);
18202 var h = (Math.cos(angle) * b);
18203 return a * b / Math.sqrt(w * w + h * h);
18204
18205 // TODO: implement distanceToBorder for database
18206 // TODO: implement distanceToBorder for triangle
18207 // TODO: implement distanceToBorder for triangleDown
18208
18209 case 'box':
18210 case 'image':
18211 case 'text':
18212 default:
18213 if (this.width) {
18214 return Math.min(
18215 Math.abs(this.width / 2 / Math.cos(angle)),
18216 Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
18217 // TODO: reckon with border radius too in case of box
18218 }
18219 else {
18220 return 0;
18221 }
18222
18223 }
18224 // TODO: implement calculation of distance to border for all shapes
18225 };
18226
18227 /**
18228 * Set forces acting on the node
18229 * @param {number} fx Force in horizontal direction
18230 * @param {number} fy Force in vertical direction
18231 */
18232 Node.prototype._setForce = function(fx, fy) {
18233 this.fx = fx;
18234 this.fy = fy;
18235 };
18236
18237 /**
18238 * Add forces acting on the node
18239 * @param {number} fx Force in horizontal direction
18240 * @param {number} fy Force in vertical direction
18241 * @private
18242 */
18243 Node.prototype._addForce = function(fx, fy) {
18244 this.fx += fx;
18245 this.fy += fy;
18246 };
18247
18248 /**
18249 * Perform one discrete step for the node
18250 * @param {number} interval Time interval in seconds
18251 */
18252 Node.prototype.discreteStep = function(interval) {
18253 if (!this.xFixed) {
18254 var dx = this.damping * this.vx; // damping force
18255 var ax = (this.fx - dx) / this.options.mass; // acceleration
18256 this.vx += ax * interval; // velocity
18257 this.x += this.vx * interval; // position
18258 }
18259 else {
18260 this.fx = 0;
18261 this.vx = 0;
18262 }
18263
18264 if (!this.yFixed) {
18265 var dy = this.damping * this.vy; // damping force
18266 var ay = (this.fy - dy) / this.options.mass; // acceleration
18267 this.vy += ay * interval; // velocity
18268 this.y += this.vy * interval; // position
18269 }
18270 else {
18271 this.fy = 0;
18272 this.vy = 0;
18273 }
18274 };
18275
18276
18277
18278 /**
18279 * Perform one discrete step for the node
18280 * @param {number} interval Time interval in seconds
18281 * @param {number} maxVelocity The speed limit imposed on the velocity
18282 */
18283 Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
18284 if (!this.xFixed) {
18285 var dx = this.damping * this.vx; // damping force
18286 var ax = (this.fx - dx) / this.options.mass; // acceleration
18287 this.vx += ax * interval; // velocity
18288 this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
18289 this.x += this.vx * interval; // position
18290 }
18291 else {
18292 this.fx = 0;
18293 this.vx = 0;
18294 }
18295
18296 if (!this.yFixed) {
18297 var dy = this.damping * this.vy; // damping force
18298 var ay = (this.fy - dy) / this.options.mass; // acceleration
18299 this.vy += ay * interval; // velocity
18300 this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
18301 this.y += this.vy * interval; // position
18302 }
18303 else {
18304 this.fy = 0;
18305 this.vy = 0;
18306 }
18307 };
18308
18309 /**
18310 * Check if this node has a fixed x and y position
18311 * @return {boolean} true if fixed, false if not
18312 */
18313 Node.prototype.isFixed = function() {
18314 return (this.xFixed && this.yFixed);
18315 };
18316
18317 /**
18318 * Check if this node is moving
18319 * @param {number} vmin the minimum velocity considered as "moving"
18320 * @return {boolean} true if moving, false if it has no velocity
18321 */
18322 Node.prototype.isMoving = function(vmin) {
18323 var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2));
18324 // this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2))
18325 return (velocity > vmin);
18326 };
18327
18328 /**
18329 * check if this node is selecte
18330 * @return {boolean} selected True if node is selected, else false
18331 */
18332 Node.prototype.isSelected = function() {
18333 return this.selected;
18334 };
18335
18336 /**
18337 * Retrieve the value of the node. Can be undefined
18338 * @return {Number} value
18339 */
18340 Node.prototype.getValue = function() {
18341 return this.value;
18342 };
18343
18344 /**
18345 * Calculate the distance from the nodes location to the given location (x,y)
18346 * @param {Number} x
18347 * @param {Number} y
18348 * @return {Number} value
18349 */
18350 Node.prototype.getDistance = function(x, y) {
18351 var dx = this.x - x,
18352 dy = this.y - y;
18353 return Math.sqrt(dx * dx + dy * dy);
18354 };
18355
18356
18357 /**
18358 * Adjust the value range of the node. The node will adjust it's radius
18359 * based on its value.
18360 * @param {Number} min
18361 * @param {Number} max
18362 */
18363 Node.prototype.setValueRange = function(min, max) {
18364 if (!this.radiusFixed && this.value !== undefined) {
18365 if (max == min) {
18366 this.options.radius= (this.options.radiusMin + this.options.radiusMax) / 2;
18367 }
18368 else {
18369 var scale = (this.options.radiusMax - this.options.radiusMin) / (max - min);
18370 this.options.radius= (this.value - min) * scale + this.options.radiusMin;
18371 }
18372 }
18373 this.baseRadiusValue = this.options.radius;
18374 };
18375
18376 /**
18377 * Draw this node in the given canvas
18378 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
18379 * @param {CanvasRenderingContext2D} ctx
18380 */
18381 Node.prototype.draw = function(ctx) {
18382 throw "Draw method not initialized for node";
18383 };
18384
18385 /**
18386 * Recalculate the size of this node in the given canvas
18387 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
18388 * @param {CanvasRenderingContext2D} ctx
18389 */
18390 Node.prototype.resize = function(ctx) {
18391 throw "Resize method not initialized for node";
18392 };
18393
18394 /**
18395 * Check if this object is overlapping with the provided object
18396 * @param {Object} obj an object with parameters left, top, right, bottom
18397 * @return {boolean} True if location is located on node
18398 */
18399 Node.prototype.isOverlappingWith = function(obj) {
18400 return (this.left < obj.right &&
18401 this.left + this.width > obj.left &&
18402 this.top < obj.bottom &&
18403 this.top + this.height > obj.top);
18404 };
18405
18406 Node.prototype._resizeImage = function (ctx) {
18407 // TODO: pre calculate the image size
18408
18409 if (!this.width || !this.height) { // undefined or 0
18410 var width, height;
18411 if (this.value) {
18412 this.options.radius= this.baseRadiusValue;
18413 var scale = this.imageObj.height / this.imageObj.width;
18414 if (scale !== undefined) {
18415 width = this.options.radius|| this.imageObj.width;
18416 height = this.options.radius* scale || this.imageObj.height;
18417 }
18418 else {
18419 width = 0;
18420 height = 0;
18421 }
18422 }
18423 else {
18424 width = this.imageObj.width;
18425 height = this.imageObj.height;
18426 }
18427 this.width = width;
18428 this.height = height;
18429
18430 this.growthIndicator = 0;
18431 if (this.width > 0 && this.height > 0) {
18432 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
18433 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
18434 this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
18435 this.growthIndicator = this.width - width;
18436 }
18437 }
18438
18439 };
18440
18441 Node.prototype._drawImage = function (ctx) {
18442 this._resizeImage(ctx);
18443
18444 this.left = this.x - this.width / 2;
18445 this.top = this.y - this.height / 2;
18446
18447 var yLabel;
18448 if (this.imageObj.width != 0 ) {
18449 // draw the shade
18450 if (this.clusterSize > 1) {
18451 var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
18452 lineWidth *= this.networkScaleInv;
18453 lineWidth = Math.min(0.2 * this.width,lineWidth);
18454
18455 ctx.globalAlpha = 0.5;
18456 ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
18457 }
18458
18459 // draw the image
18460 ctx.globalAlpha = 1.0;
18461 ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
18462 yLabel = this.y + this.height / 2;
18463 }
18464 else {
18465 // image still loading... just draw the label for now
18466 yLabel = this.y;
18467 }
18468
18469 this._label(ctx, this.label, this.x, yLabel, undefined, "top");
18470 };
18471
18472
18473 Node.prototype._resizeBox = function (ctx) {
18474 if (!this.width) {
18475 var margin = 5;
18476 var textSize = this.getTextSize(ctx);
18477 this.width = textSize.width + 2 * margin;
18478 this.height = textSize.height + 2 * margin;
18479
18480 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
18481 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
18482 this.growthIndicator = this.width - (textSize.width + 2 * margin);
18483 // this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
18484
18485 }
18486 };
18487
18488 Node.prototype._drawBox = function (ctx) {
18489 this._resizeBox(ctx);
18490
18491 this.left = this.x - this.width / 2;
18492 this.top = this.y - this.height / 2;
18493
18494 var clusterLineWidth = 2.5;
18495 var borderWidth = this.options.borderWidth;
18496 var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
18497
18498 ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
18499
18500 // draw the outer border
18501 if (this.clusterSize > 1) {
18502 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18503 ctx.lineWidth *= this.networkScaleInv;
18504 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18505
18506 ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
18507 ctx.stroke();
18508 }
18509 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18510 ctx.lineWidth *= this.networkScaleInv;
18511 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18512
18513 ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.options.color.background;
18514
18515 ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius);
18516 ctx.fill();
18517 ctx.stroke();
18518
18519 this._label(ctx, this.label, this.x, this.y);
18520 };
18521
18522
18523 Node.prototype._resizeDatabase = function (ctx) {
18524 if (!this.width) {
18525 var margin = 5;
18526 var textSize = this.getTextSize(ctx);
18527 var size = textSize.width + 2 * margin;
18528 this.width = size;
18529 this.height = size;
18530
18531 // scaling used for clustering
18532 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
18533 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
18534 this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
18535 this.growthIndicator = this.width - size;
18536 }
18537 };
18538
18539 Node.prototype._drawDatabase = function (ctx) {
18540 this._resizeDatabase(ctx);
18541 this.left = this.x - this.width / 2;
18542 this.top = this.y - this.height / 2;
18543
18544 var clusterLineWidth = 2.5;
18545 var borderWidth = this.options.borderWidth;
18546 var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
18547
18548 ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
18549
18550 // draw the outer border
18551 if (this.clusterSize > 1) {
18552 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18553 ctx.lineWidth *= this.networkScaleInv;
18554 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18555
18556 ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
18557 ctx.stroke();
18558 }
18559 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18560 ctx.lineWidth *= this.networkScaleInv;
18561 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18562
18563 ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
18564 ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
18565 ctx.fill();
18566 ctx.stroke();
18567
18568 this._label(ctx, this.label, this.x, this.y);
18569 };
18570
18571
18572 Node.prototype._resizeCircle = function (ctx) {
18573 if (!this.width) {
18574 var margin = 5;
18575 var textSize = this.getTextSize(ctx);
18576 var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
18577 this.options.radius = diameter / 2;
18578
18579 this.width = diameter;
18580 this.height = diameter;
18581
18582 // scaling used for clustering
18583 // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
18584 // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
18585 this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
18586 this.growthIndicator = this.options.radius- 0.5*diameter;
18587 }
18588 };
18589
18590 Node.prototype._drawCircle = function (ctx) {
18591 this._resizeCircle(ctx);
18592 this.left = this.x - this.width / 2;
18593 this.top = this.y - this.height / 2;
18594
18595 var clusterLineWidth = 2.5;
18596 var borderWidth = this.options.borderWidth;
18597 var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
18598
18599 ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
18600
18601 // draw the outer border
18602 if (this.clusterSize > 1) {
18603 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18604 ctx.lineWidth *= this.networkScaleInv;
18605 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18606
18607 ctx.circle(this.x, this.y, this.options.radius+2*ctx.lineWidth);
18608 ctx.stroke();
18609 }
18610 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18611 ctx.lineWidth *= this.networkScaleInv;
18612 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18613
18614 ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
18615 ctx.circle(this.x, this.y, this.options.radius);
18616 ctx.fill();
18617 ctx.stroke();
18618
18619 this._label(ctx, this.label, this.x, this.y);
18620 };
18621
18622 Node.prototype._resizeEllipse = function (ctx) {
18623 if (!this.width) {
18624 var textSize = this.getTextSize(ctx);
18625
18626 this.width = textSize.width * 1.5;
18627 this.height = textSize.height * 2;
18628 if (this.width < this.height) {
18629 this.width = this.height;
18630 }
18631 var defaultSize = this.width;
18632
18633 // scaling used for clustering
18634 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
18635 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
18636 this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
18637 this.growthIndicator = this.width - defaultSize;
18638 }
18639 };
18640
18641 Node.prototype._drawEllipse = function (ctx) {
18642 this._resizeEllipse(ctx);
18643 this.left = this.x - this.width / 2;
18644 this.top = this.y - this.height / 2;
18645
18646 var clusterLineWidth = 2.5;
18647 var borderWidth = this.options.borderWidth;
18648 var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
18649
18650 ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
18651
18652 // draw the outer border
18653 if (this.clusterSize > 1) {
18654 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18655 ctx.lineWidth *= this.networkScaleInv;
18656 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18657
18658 ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
18659 ctx.stroke();
18660 }
18661 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18662 ctx.lineWidth *= this.networkScaleInv;
18663 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18664
18665 ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
18666
18667 ctx.ellipse(this.left, this.top, this.width, this.height);
18668 ctx.fill();
18669 ctx.stroke();
18670 this._label(ctx, this.label, this.x, this.y);
18671 };
18672
18673 Node.prototype._drawDot = function (ctx) {
18674 this._drawShape(ctx, 'circle');
18675 };
18676
18677 Node.prototype._drawTriangle = function (ctx) {
18678 this._drawShape(ctx, 'triangle');
18679 };
18680
18681 Node.prototype._drawTriangleDown = function (ctx) {
18682 this._drawShape(ctx, 'triangleDown');
18683 };
18684
18685 Node.prototype._drawSquare = function (ctx) {
18686 this._drawShape(ctx, 'square');
18687 };
18688
18689 Node.prototype._drawStar = function (ctx) {
18690 this._drawShape(ctx, 'star');
18691 };
18692
18693 Node.prototype._resizeShape = function (ctx) {
18694 if (!this.width) {
18695 this.options.radius= this.baseRadiusValue;
18696 var size = 2 * this.options.radius;
18697 this.width = size;
18698 this.height = size;
18699
18700 // scaling used for clustering
18701 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
18702 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
18703 this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
18704 this.growthIndicator = this.width - size;
18705 }
18706 };
18707
18708 Node.prototype._drawShape = function (ctx, shape) {
18709 this._resizeShape(ctx);
18710
18711 this.left = this.x - this.width / 2;
18712 this.top = this.y - this.height / 2;
18713
18714 var clusterLineWidth = 2.5;
18715 var borderWidth = this.options.borderWidth;
18716 var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
18717 var radiusMultiplier = 2;
18718
18719 // choose draw method depending on the shape
18720 switch (shape) {
18721 case 'dot': radiusMultiplier = 2; break;
18722 case 'square': radiusMultiplier = 2; break;
18723 case 'triangle': radiusMultiplier = 3; break;
18724 case 'triangleDown': radiusMultiplier = 3; break;
18725 case 'star': radiusMultiplier = 4; break;
18726 }
18727
18728 ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
18729 // draw the outer border
18730 if (this.clusterSize > 1) {
18731 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18732 ctx.lineWidth *= this.networkScaleInv;
18733 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18734
18735 ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth);
18736 ctx.stroke();
18737 }
18738 ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
18739 ctx.lineWidth *= this.networkScaleInv;
18740 ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
18741
18742 ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
18743 ctx[shape](this.x, this.y, this.options.radius);
18744 ctx.fill();
18745 ctx.stroke();
18746
18747 if (this.label) {
18748 this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
18749 }
18750 };
18751
18752 Node.prototype._resizeText = function (ctx) {
18753 if (!this.width) {
18754 var margin = 5;
18755 var textSize = this.getTextSize(ctx);
18756 this.width = textSize.width + 2 * margin;
18757 this.height = textSize.height + 2 * margin;
18758
18759 // scaling used for clustering
18760 this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
18761 this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
18762 this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
18763 this.growthIndicator = this.width - (textSize.width + 2 * margin);
18764 }
18765 };
18766
18767 Node.prototype._drawText = function (ctx) {
18768 this._resizeText(ctx);
18769 this.left = this.x - this.width / 2;
18770 this.top = this.y - this.height / 2;
18771
18772 this._label(ctx, this.label, this.x, this.y);
18773 };
18774
18775
18776 Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
18777 if (text && Number(this.options.fontSize) * this.networkScale > this.fontDrawThreshold) {
18778 ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
18779 ctx.textAlign = align || "center";
18780 ctx.textBaseline = baseline || "middle";
18781
18782 var lines = text.split('\n');
18783 var lineCount = lines.length;
18784 var fontSize = (Number(this.options.fontSize) + 4);
18785 var yLine = y + (1 - lineCount) / 2 * fontSize;
18786 if (labelUnderNode == true) {
18787 yLine = y + (1 - lineCount) / (2 * fontSize);
18788 }
18789
18790 // font fill from edges now for nodes!
18791 if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
18792 var width = ctx.measureText(lines[0]).width;
18793 for (var i = 1; i < lineCount; i++) {
18794 var lineWidth = ctx.measureText(lines[i]).width;
18795 width = lineWidth > width ? lineWidth : width;
18796 }
18797 var height = this.options.fontSize * lineCount;
18798 var left = x - width / 2;
18799 var top = y - height / 2;
18800 if (ctx.textBaseline == "top") {
18801 top += 0.5 * fontSize;
18802 }
18803 ctx.fillStyle = this.options.fontFill;
18804 ctx.fillRect(left, top, width, height);
18805 }
18806
18807 // draw text
18808 ctx.fillStyle = this.options.fontColor || "black";
18809 for (var i = 0; i < lineCount; i++) {
18810 ctx.fillText(lines[i], x, yLine);
18811 yLine += fontSize;
18812 }
18813 }
18814 };
18815
18816
18817 Node.prototype.getTextSize = function(ctx) {
18818 if (this.label !== undefined) {
18819 ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
18820
18821 var lines = this.label.split('\n'),
18822 height = (Number(this.options.fontSize) + 4) * lines.length,
18823 width = 0;
18824
18825 for (var i = 0, iMax = lines.length; i < iMax; i++) {
18826 width = Math.max(width, ctx.measureText(lines[i]).width);
18827 }
18828
18829 return {"width": width, "height": height};
18830 }
18831 else {
18832 return {"width": 0, "height": 0};
18833 }
18834 };
18835
18836 /**
18837 * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
18838 * there is a safety margin of 0.3 * width;
18839 *
18840 * @returns {boolean}
18841 */
18842 Node.prototype.inArea = function() {
18843 if (this.width !== undefined) {
18844 return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
18845 this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
18846 this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
18847 this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
18848 }
18849 else {
18850 return true;
18851 }
18852 };
18853
18854 /**
18855 * checks if the core of the node is in the display area, this is used for opening clusters around zoom
18856 * @returns {boolean}
18857 */
18858 Node.prototype.inView = function() {
18859 return (this.x >= this.canvasTopLeft.x &&
18860 this.x < this.canvasBottomRight.x &&
18861 this.y >= this.canvasTopLeft.y &&
18862 this.y < this.canvasBottomRight.y);
18863 };
18864
18865 /**
18866 * This allows the zoom level of the network to influence the rendering
18867 * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
18868 *
18869 * @param scale
18870 * @param canvasTopLeft
18871 * @param canvasBottomRight
18872 */
18873 Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
18874 this.networkScaleInv = 1.0/scale;
18875 this.networkScale = scale;
18876 this.canvasTopLeft = canvasTopLeft;
18877 this.canvasBottomRight = canvasBottomRight;
18878 };
18879
18880
18881 /**
18882 * This allows the zoom level of the network to influence the rendering
18883 *
18884 * @param scale
18885 */
18886 Node.prototype.setScale = function(scale) {
18887 this.networkScaleInv = 1.0/scale;
18888 this.networkScale = scale;
18889 };
18890
18891
18892
18893 /**
18894 * set the velocity at 0. Is called when this node is contained in another during clustering
18895 */
18896 Node.prototype.clearVelocity = function() {
18897 this.vx = 0;
18898 this.vy = 0;
18899 };
18900
18901
18902 /**
18903 * Basic preservation of (kinectic) energy
18904 *
18905 * @param massBeforeClustering
18906 */
18907 Node.prototype.updateVelocity = function(massBeforeClustering) {
18908 var energyBefore = this.vx * this.vx * massBeforeClustering;
18909 //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
18910 this.vx = Math.sqrt(energyBefore/this.options.mass);
18911 energyBefore = this.vy * this.vy * massBeforeClustering;
18912 //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
18913 this.vy = Math.sqrt(energyBefore/this.options.mass);
18914 };
18915
18916 module.exports = Node;
18917
18918
18919/***/ },
18920/* 38 */
18921/***/ function(module, exports, __webpack_require__) {
18922
18923 /**
18924 * Popup is a class to create a popup window with some text
18925 * @param {Element} container The container object.
18926 * @param {Number} [x]
18927 * @param {Number} [y]
18928 * @param {String} [text]
18929 * @param {Object} [style] An object containing borderColor,
18930 * backgroundColor, etc.
18931 */
18932 function Popup(container, x, y, text, style) {
18933 if (container) {
18934 this.container = container;
18935 }
18936 else {
18937 this.container = document.body;
18938 }
18939
18940 // x, y and text are optional, see if a style object was passed in their place
18941 if (style === undefined) {
18942 if (typeof x === "object") {
18943 style = x;
18944 x = undefined;
18945 } else if (typeof text === "object") {
18946 style = text;
18947 text = undefined;
18948 } else {
18949 // for backwards compatibility, in case clients other than Network are creating Popup directly
18950 style = {
18951 fontColor: 'black',
18952 fontSize: 14, // px
18953 fontFace: 'verdana',
18954 color: {
18955 border: '#666',
18956 background: '#FFFFC6'
18957 }
18958 }
18959 }
18960 }
18961
18962 this.x = 0;
18963 this.y = 0;
18964 this.padding = 5;
18965
18966 if (x !== undefined && y !== undefined ) {
18967 this.setPosition(x, y);
18968 }
18969 if (text !== undefined) {
18970 this.setText(text);
18971 }
18972
18973 // create the frame
18974 this.frame = document.createElement("div");
18975 var styleAttr = this.frame.style;
18976 styleAttr.position = "absolute";
18977 styleAttr.visibility = "hidden";
18978 styleAttr.border = "1px solid " + style.color.border;
18979 styleAttr.color = style.fontColor;
18980 styleAttr.fontSize = style.fontSize + "px";
18981 styleAttr.fontFamily = style.fontFace;
18982 styleAttr.padding = this.padding + "px";
18983 styleAttr.backgroundColor = style.color.background;
18984 styleAttr.borderRadius = "3px";
18985 styleAttr.MozBorderRadius = "3px";
18986 styleAttr.WebkitBorderRadius = "3px";
18987 styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
18988 styleAttr.whiteSpace = "nowrap";
18989 this.container.appendChild(this.frame);
18990 }
18991
18992 /**
18993 * @param {number} x Horizontal position of the popup window
18994 * @param {number} y Vertical position of the popup window
18995 */
18996 Popup.prototype.setPosition = function(x, y) {
18997 this.x = parseInt(x);
18998 this.y = parseInt(y);
18999 };
19000
19001 /**
19002 * Set the text for the popup window. This can be HTML code
19003 * @param {string} text
19004 */
19005 Popup.prototype.setText = function(text) {
19006 this.frame.innerHTML = text;
19007 };
19008
19009 /**
19010 * Show the popup window
19011 * @param {boolean} show Optional. Show or hide the window
19012 */
19013 Popup.prototype.show = function (show) {
19014 if (show === undefined) {
19015 show = true;
19016 }
19017
19018 if (show) {
19019 var height = this.frame.clientHeight;
19020 var width = this.frame.clientWidth;
19021 var maxHeight = this.frame.parentNode.clientHeight;
19022 var maxWidth = this.frame.parentNode.clientWidth;
19023
19024 var top = (this.y - height);
19025 if (top + height + this.padding > maxHeight) {
19026 top = maxHeight - height - this.padding;
19027 }
19028 if (top < this.padding) {
19029 top = this.padding;
19030 }
19031
19032 var left = this.x;
19033 if (left + width + this.padding > maxWidth) {
19034 left = maxWidth - width - this.padding;
19035 }
19036 if (left < this.padding) {
19037 left = this.padding;
19038 }
19039
19040 this.frame.style.left = left + "px";
19041 this.frame.style.top = top + "px";
19042 this.frame.style.visibility = "visible";
19043 }
19044 else {
19045 this.hide();
19046 }
19047 };
19048
19049 /**
19050 * Hide the popup window
19051 */
19052 Popup.prototype.hide = function () {
19053 this.frame.style.visibility = "hidden";
19054 };
19055
19056 module.exports = Popup;
19057
19058
19059/***/ },
19060/* 39 */
19061/***/ function(module, exports, __webpack_require__) {
19062
19063 /**
19064 * Parse a text source containing data in DOT language into a JSON object.
19065 * The object contains two lists: one with nodes and one with edges.
19066 *
19067 * DOT language reference: http://www.graphviz.org/doc/info/lang.html
19068 *
19069 * @param {String} data Text containing a graph in DOT-notation
19070 * @return {Object} graph An object containing two parameters:
19071 * {Object[]} nodes
19072 * {Object[]} edges
19073 */
19074 function parseDOT (data) {
19075 dot = data;
19076 return parseGraph();
19077 }
19078
19079 // token types enumeration
19080 var TOKENTYPE = {
19081 NULL : 0,
19082 DELIMITER : 1,
19083 IDENTIFIER: 2,
19084 UNKNOWN : 3
19085 };
19086
19087 // map with all delimiters
19088 var DELIMITERS = {
19089 '{': true,
19090 '}': true,
19091 '[': true,
19092 ']': true,
19093 ';': true,
19094 '=': true,
19095 ',': true,
19096
19097 '->': true,
19098 '--': true
19099 };
19100
19101 var dot = ''; // current dot file
19102 var index = 0; // current index in dot file
19103 var c = ''; // current token character in expr
19104 var token = ''; // current token
19105 var tokenType = TOKENTYPE.NULL; // type of the token
19106
19107 /**
19108 * Get the first character from the dot file.
19109 * The character is stored into the char c. If the end of the dot file is
19110 * reached, the function puts an empty string in c.
19111 */
19112 function first() {
19113 index = 0;
19114 c = dot.charAt(0);
19115 }
19116
19117 /**
19118 * Get the next character from the dot file.
19119 * The character is stored into the char c. If the end of the dot file is
19120 * reached, the function puts an empty string in c.
19121 */
19122 function next() {
19123 index++;
19124 c = dot.charAt(index);
19125 }
19126
19127 /**
19128 * Preview the next character from the dot file.
19129 * @return {String} cNext
19130 */
19131 function nextPreview() {
19132 return dot.charAt(index + 1);
19133 }
19134
19135 /**
19136 * Test whether given character is alphabetic or numeric
19137 * @param {String} c
19138 * @return {Boolean} isAlphaNumeric
19139 */
19140 var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
19141 function isAlphaNumeric(c) {
19142 return regexAlphaNumeric.test(c);
19143 }
19144
19145 /**
19146 * Merge all properties of object b into object b
19147 * @param {Object} a
19148 * @param {Object} b
19149 * @return {Object} a
19150 */
19151 function merge (a, b) {
19152 if (!a) {
19153 a = {};
19154 }
19155
19156 if (b) {
19157 for (var name in b) {
19158 if (b.hasOwnProperty(name)) {
19159 a[name] = b[name];
19160 }
19161 }
19162 }
19163 return a;
19164 }
19165
19166 /**
19167 * Set a value in an object, where the provided parameter name can be a
19168 * path with nested parameters. For example:
19169 *
19170 * var obj = {a: 2};
19171 * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
19172 *
19173 * @param {Object} obj
19174 * @param {String} path A parameter name or dot-separated parameter path,
19175 * like "color.highlight.border".
19176 * @param {*} value
19177 */
19178 function setValue(obj, path, value) {
19179 var keys = path.split('.');
19180 var o = obj;
19181 while (keys.length) {
19182 var key = keys.shift();
19183 if (keys.length) {
19184 // this isn't the end point
19185 if (!o[key]) {
19186 o[key] = {};
19187 }
19188 o = o[key];
19189 }
19190 else {
19191 // this is the end point
19192 o[key] = value;
19193 }
19194 }
19195 }
19196
19197 /**
19198 * Add a node to a graph object. If there is already a node with
19199 * the same id, their attributes will be merged.
19200 * @param {Object} graph
19201 * @param {Object} node
19202 */
19203 function addNode(graph, node) {
19204 var i, len;
19205 var current = null;
19206
19207 // find root graph (in case of subgraph)
19208 var graphs = [graph]; // list with all graphs from current graph to root graph
19209 var root = graph;
19210 while (root.parent) {
19211 graphs.push(root.parent);
19212 root = root.parent;
19213 }
19214
19215 // find existing node (at root level) by its id
19216 if (root.nodes) {
19217 for (i = 0, len = root.nodes.length; i < len; i++) {
19218 if (node.id === root.nodes[i].id) {
19219 current = root.nodes[i];
19220 break;
19221 }
19222 }
19223 }
19224
19225 if (!current) {
19226 // this is a new node
19227 current = {
19228 id: node.id
19229 };
19230 if (graph.node) {
19231 // clone default attributes
19232 current.attr = merge(current.attr, graph.node);
19233 }
19234 }
19235
19236 // add node to this (sub)graph and all its parent graphs
19237 for (i = graphs.length - 1; i >= 0; i--) {
19238 var g = graphs[i];
19239
19240 if (!g.nodes) {
19241 g.nodes = [];
19242 }
19243 if (g.nodes.indexOf(current) == -1) {
19244 g.nodes.push(current);
19245 }
19246 }
19247
19248 // merge attributes
19249 if (node.attr) {
19250 current.attr = merge(current.attr, node.attr);
19251 }
19252 }
19253
19254 /**
19255 * Add an edge to a graph object
19256 * @param {Object} graph
19257 * @param {Object} edge
19258 */
19259 function addEdge(graph, edge) {
19260 if (!graph.edges) {
19261 graph.edges = [];
19262 }
19263 graph.edges.push(edge);
19264 if (graph.edge) {
19265 var attr = merge({}, graph.edge); // clone default attributes
19266 edge.attr = merge(attr, edge.attr); // merge attributes
19267 }
19268 }
19269
19270 /**
19271 * Create an edge to a graph object
19272 * @param {Object} graph
19273 * @param {String | Number | Object} from
19274 * @param {String | Number | Object} to
19275 * @param {String} type
19276 * @param {Object | null} attr
19277 * @return {Object} edge
19278 */
19279 function createEdge(graph, from, to, type, attr) {
19280 var edge = {
19281 from: from,
19282 to: to,
19283 type: type
19284 };
19285
19286 if (graph.edge) {
19287 edge.attr = merge({}, graph.edge); // clone default attributes
19288 }
19289 edge.attr = merge(edge.attr || {}, attr); // merge attributes
19290
19291 return edge;
19292 }
19293
19294 /**
19295 * Get next token in the current dot file.
19296 * The token and token type are available as token and tokenType
19297 */
19298 function getToken() {
19299 tokenType = TOKENTYPE.NULL;
19300 token = '';
19301
19302 // skip over whitespaces
19303 while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
19304 next();
19305 }
19306
19307 do {
19308 var isComment = false;
19309
19310 // skip comment
19311 if (c == '#') {
19312 // find the previous non-space character
19313 var i = index - 1;
19314 while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
19315 i--;
19316 }
19317 if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
19318 // the # is at the start of a line, this is indeed a line comment
19319 while (c != '' && c != '\n') {
19320 next();
19321 }
19322 isComment = true;
19323 }
19324 }
19325 if (c == '/' && nextPreview() == '/') {
19326 // skip line comment
19327 while (c != '' && c != '\n') {
19328 next();
19329 }
19330 isComment = true;
19331 }
19332 if (c == '/' && nextPreview() == '*') {
19333 // skip block comment
19334 while (c != '') {
19335 if (c == '*' && nextPreview() == '/') {
19336 // end of block comment found. skip these last two characters
19337 next();
19338 next();
19339 break;
19340 }
19341 else {
19342 next();
19343 }
19344 }
19345 isComment = true;
19346 }
19347
19348 // skip over whitespaces
19349 while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
19350 next();
19351 }
19352 }
19353 while (isComment);
19354
19355 // check for end of dot file
19356 if (c == '') {
19357 // token is still empty
19358 tokenType = TOKENTYPE.DELIMITER;
19359 return;
19360 }
19361
19362 // check for delimiters consisting of 2 characters
19363 var c2 = c + nextPreview();
19364 if (DELIMITERS[c2]) {
19365 tokenType = TOKENTYPE.DELIMITER;
19366 token = c2;
19367 next();
19368 next();
19369 return;
19370 }
19371
19372 // check for delimiters consisting of 1 character
19373 if (DELIMITERS[c]) {
19374 tokenType = TOKENTYPE.DELIMITER;
19375 token = c;
19376 next();
19377 return;
19378 }
19379
19380 // check for an identifier (number or string)
19381 // TODO: more precise parsing of numbers/strings (and the port separator ':')
19382 if (isAlphaNumeric(c) || c == '-') {
19383 token += c;
19384 next();
19385
19386 while (isAlphaNumeric(c)) {
19387 token += c;
19388 next();
19389 }
19390 if (token == 'false') {
19391 token = false; // convert to boolean
19392 }
19393 else if (token == 'true') {
19394 token = true; // convert to boolean
19395 }
19396 else if (!isNaN(Number(token))) {
19397 token = Number(token); // convert to number
19398 }
19399 tokenType = TOKENTYPE.IDENTIFIER;
19400 return;
19401 }
19402
19403 // check for a string enclosed by double quotes
19404 if (c == '"') {
19405 next();
19406 while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
19407 token += c;
19408 if (c == '"') { // skip the escape character
19409 next();
19410 }
19411 next();
19412 }
19413 if (c != '"') {
19414 throw newSyntaxError('End of string " expected');
19415 }
19416 next();
19417 tokenType = TOKENTYPE.IDENTIFIER;
19418 return;
19419 }
19420
19421 // something unknown is found, wrong characters, a syntax error
19422 tokenType = TOKENTYPE.UNKNOWN;
19423 while (c != '') {
19424 token += c;
19425 next();
19426 }
19427 throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
19428 }
19429
19430 /**
19431 * Parse a graph.
19432 * @returns {Object} graph
19433 */
19434 function parseGraph() {
19435 var graph = {};
19436
19437 first();
19438 getToken();
19439
19440 // optional strict keyword
19441 if (token == 'strict') {
19442 graph.strict = true;
19443 getToken();
19444 }
19445
19446 // graph or digraph keyword
19447 if (token == 'graph' || token == 'digraph') {
19448 graph.type = token;
19449 getToken();
19450 }
19451
19452 // optional graph id
19453 if (tokenType == TOKENTYPE.IDENTIFIER) {
19454 graph.id = token;
19455 getToken();
19456 }
19457
19458 // open angle bracket
19459 if (token != '{') {
19460 throw newSyntaxError('Angle bracket { expected');
19461 }
19462 getToken();
19463
19464 // statements
19465 parseStatements(graph);
19466
19467 // close angle bracket
19468 if (token != '}') {
19469 throw newSyntaxError('Angle bracket } expected');
19470 }
19471 getToken();
19472
19473 // end of file
19474 if (token !== '') {
19475 throw newSyntaxError('End of file expected');
19476 }
19477 getToken();
19478
19479 // remove temporary default properties
19480 delete graph.node;
19481 delete graph.edge;
19482 delete graph.graph;
19483
19484 return graph;
19485 }
19486
19487 /**
19488 * Parse a list with statements.
19489 * @param {Object} graph
19490 */
19491 function parseStatements (graph) {
19492 while (token !== '' && token != '}') {
19493 parseStatement(graph);
19494 if (token == ';') {
19495 getToken();
19496 }
19497 }
19498 }
19499
19500 /**
19501 * Parse a single statement. Can be a an attribute statement, node
19502 * statement, a series of node statements and edge statements, or a
19503 * parameter.
19504 * @param {Object} graph
19505 */
19506 function parseStatement(graph) {
19507 // parse subgraph
19508 var subgraph = parseSubgraph(graph);
19509 if (subgraph) {
19510 // edge statements
19511 parseEdge(graph, subgraph);
19512
19513 return;
19514 }
19515
19516 // parse an attribute statement
19517 var attr = parseAttributeStatement(graph);
19518 if (attr) {
19519 return;
19520 }
19521
19522 // parse node
19523 if (tokenType != TOKENTYPE.IDENTIFIER) {
19524 throw newSyntaxError('Identifier expected');
19525 }
19526 var id = token; // id can be a string or a number
19527 getToken();
19528
19529 if (token == '=') {
19530 // id statement
19531 getToken();
19532 if (tokenType != TOKENTYPE.IDENTIFIER) {
19533 throw newSyntaxError('Identifier expected');
19534 }
19535 graph[id] = token;
19536 getToken();
19537 // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
19538 }
19539 else {
19540 parseNodeStatement(graph, id);
19541 }
19542 }
19543
19544 /**
19545 * Parse a subgraph
19546 * @param {Object} graph parent graph object
19547 * @return {Object | null} subgraph
19548 */
19549 function parseSubgraph (graph) {
19550 var subgraph = null;
19551
19552 // optional subgraph keyword
19553 if (token == 'subgraph') {
19554 subgraph = {};
19555 subgraph.type = 'subgraph';
19556 getToken();
19557
19558 // optional graph id
19559 if (tokenType == TOKENTYPE.IDENTIFIER) {
19560 subgraph.id = token;
19561 getToken();
19562 }
19563 }
19564
19565 // open angle bracket
19566 if (token == '{') {
19567 getToken();
19568
19569 if (!subgraph) {
19570 subgraph = {};
19571 }
19572 subgraph.parent = graph;
19573 subgraph.node = graph.node;
19574 subgraph.edge = graph.edge;
19575 subgraph.graph = graph.graph;
19576
19577 // statements
19578 parseStatements(subgraph);
19579
19580 // close angle bracket
19581 if (token != '}') {
19582 throw newSyntaxError('Angle bracket } expected');
19583 }
19584 getToken();
19585
19586 // remove temporary default properties
19587 delete subgraph.node;
19588 delete subgraph.edge;
19589 delete subgraph.graph;
19590 delete subgraph.parent;
19591
19592 // register at the parent graph
19593 if (!graph.subgraphs) {
19594 graph.subgraphs = [];
19595 }
19596 graph.subgraphs.push(subgraph);
19597 }
19598
19599 return subgraph;
19600 }
19601
19602 /**
19603 * parse an attribute statement like "node [shape=circle fontSize=16]".
19604 * Available keywords are 'node', 'edge', 'graph'.
19605 * The previous list with default attributes will be replaced
19606 * @param {Object} graph
19607 * @returns {String | null} keyword Returns the name of the parsed attribute
19608 * (node, edge, graph), or null if nothing
19609 * is parsed.
19610 */
19611 function parseAttributeStatement (graph) {
19612 // attribute statements
19613 if (token == 'node') {
19614 getToken();
19615
19616 // node attributes
19617 graph.node = parseAttributeList();
19618 return 'node';
19619 }
19620 else if (token == 'edge') {
19621 getToken();
19622
19623 // edge attributes
19624 graph.edge = parseAttributeList();
19625 return 'edge';
19626 }
19627 else if (token == 'graph') {
19628 getToken();
19629
19630 // graph attributes
19631 graph.graph = parseAttributeList();
19632 return 'graph';
19633 }
19634
19635 return null;
19636 }
19637
19638 /**
19639 * parse a node statement
19640 * @param {Object} graph
19641 * @param {String | Number} id
19642 */
19643 function parseNodeStatement(graph, id) {
19644 // node statement
19645 var node = {
19646 id: id
19647 };
19648 var attr = parseAttributeList();
19649 if (attr) {
19650 node.attr = attr;
19651 }
19652 addNode(graph, node);
19653
19654 // edge statements
19655 parseEdge(graph, id);
19656 }
19657
19658 /**
19659 * Parse an edge or a series of edges
19660 * @param {Object} graph
19661 * @param {String | Number} from Id of the from node
19662 */
19663 function parseEdge(graph, from) {
19664 while (token == '->' || token == '--') {
19665 var to;
19666 var type = token;
19667 getToken();
19668
19669 var subgraph = parseSubgraph(graph);
19670 if (subgraph) {
19671 to = subgraph;
19672 }
19673 else {
19674 if (tokenType != TOKENTYPE.IDENTIFIER) {
19675 throw newSyntaxError('Identifier or subgraph expected');
19676 }
19677 to = token;
19678 addNode(graph, {
19679 id: to
19680 });
19681 getToken();
19682 }
19683
19684 // parse edge attributes
19685 var attr = parseAttributeList();
19686
19687 // create edge
19688 var edge = createEdge(graph, from, to, type, attr);
19689 addEdge(graph, edge);
19690
19691 from = to;
19692 }
19693 }
19694
19695 /**
19696 * Parse a set with attributes,
19697 * for example [label="1.000", shape=solid]
19698 * @return {Object | null} attr
19699 */
19700 function parseAttributeList() {
19701 var attr = null;
19702
19703 while (token == '[') {
19704 getToken();
19705 attr = {};
19706 while (token !== '' && token != ']') {
19707 if (tokenType != TOKENTYPE.IDENTIFIER) {
19708 throw newSyntaxError('Attribute name expected');
19709 }
19710 var name = token;
19711
19712 getToken();
19713 if (token != '=') {
19714 throw newSyntaxError('Equal sign = expected');
19715 }
19716 getToken();
19717
19718 if (tokenType != TOKENTYPE.IDENTIFIER) {
19719 throw newSyntaxError('Attribute value expected');
19720 }
19721 var value = token;
19722 setValue(attr, name, value); // name can be a path
19723
19724 getToken();
19725 if (token ==',') {
19726 getToken();
19727 }
19728 }
19729
19730 if (token != ']') {
19731 throw newSyntaxError('Bracket ] expected');
19732 }
19733 getToken();
19734 }
19735
19736 return attr;
19737 }
19738
19739 /**
19740 * Create a syntax error with extra information on current token and index.
19741 * @param {String} message
19742 * @returns {SyntaxError} err
19743 */
19744 function newSyntaxError(message) {
19745 return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
19746 }
19747
19748 /**
19749 * Chop off text after a maximum length
19750 * @param {String} text
19751 * @param {Number} maxLength
19752 * @returns {String}
19753 */
19754 function chop (text, maxLength) {
19755 return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
19756 }
19757
19758 /**
19759 * Execute a function fn for each pair of elements in two arrays
19760 * @param {Array | *} array1
19761 * @param {Array | *} array2
19762 * @param {function} fn
19763 */
19764 function forEach2(array1, array2, fn) {
19765 if (array1 instanceof Array) {
19766 array1.forEach(function (elem1) {
19767 if (array2 instanceof Array) {
19768 array2.forEach(function (elem2) {
19769 fn(elem1, elem2);
19770 });
19771 }
19772 else {
19773 fn(elem1, array2);
19774 }
19775 });
19776 }
19777 else {
19778 if (array2 instanceof Array) {
19779 array2.forEach(function (elem2) {
19780 fn(array1, elem2);
19781 });
19782 }
19783 else {
19784 fn(array1, array2);
19785 }
19786 }
19787 }
19788
19789 /**
19790 * Convert a string containing a graph in DOT language into a map containing
19791 * with nodes and edges in the format of graph.
19792 * @param {String} data Text containing a graph in DOT-notation
19793 * @return {Object} graphData
19794 */
19795 function DOTToGraph (data) {
19796 // parse the DOT file
19797 var dotData = parseDOT(data);
19798 var graphData = {
19799 nodes: [],
19800 edges: [],
19801 options: {}
19802 };
19803
19804 // copy the nodes
19805 if (dotData.nodes) {
19806 dotData.nodes.forEach(function (dotNode) {
19807 var graphNode = {
19808 id: dotNode.id,
19809 label: String(dotNode.label || dotNode.id)
19810 };
19811 merge(graphNode, dotNode.attr);
19812 if (graphNode.image) {
19813 graphNode.shape = 'image';
19814 }
19815 graphData.nodes.push(graphNode);
19816 });
19817 }
19818
19819 // copy the edges
19820 if (dotData.edges) {
19821 /**
19822 * Convert an edge in DOT format to an edge with VisGraph format
19823 * @param {Object} dotEdge
19824 * @returns {Object} graphEdge
19825 */
19826 function convertEdge(dotEdge) {
19827 var graphEdge = {
19828 from: dotEdge.from,
19829 to: dotEdge.to
19830 };
19831 merge(graphEdge, dotEdge.attr);
19832 graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
19833 return graphEdge;
19834 }
19835
19836 dotData.edges.forEach(function (dotEdge) {
19837 var from, to;
19838 if (dotEdge.from instanceof Object) {
19839 from = dotEdge.from.nodes;
19840 }
19841 else {
19842 from = {
19843 id: dotEdge.from
19844 }
19845 }
19846
19847 if (dotEdge.to instanceof Object) {
19848 to = dotEdge.to.nodes;
19849 }
19850 else {
19851 to = {
19852 id: dotEdge.to
19853 }
19854 }
19855
19856 if (dotEdge.from instanceof Object && dotEdge.from.edges) {
19857 dotEdge.from.edges.forEach(function (subEdge) {
19858 var graphEdge = convertEdge(subEdge);
19859 graphData.edges.push(graphEdge);
19860 });
19861 }
19862
19863 forEach2(from, to, function (from, to) {
19864 var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
19865 var graphEdge = convertEdge(subEdge);
19866 graphData.edges.push(graphEdge);
19867 });
19868
19869 if (dotEdge.to instanceof Object && dotEdge.to.edges) {
19870 dotEdge.to.edges.forEach(function (subEdge) {
19871 var graphEdge = convertEdge(subEdge);
19872 graphData.edges.push(graphEdge);
19873 });
19874 }
19875 });
19876 }
19877
19878 // copy the options
19879 if (dotData.attr) {
19880 graphData.options = dotData.attr;
19881 }
19882
19883 return graphData;
19884 }
19885
19886 // exports
19887 exports.parseDOT = parseDOT;
19888 exports.DOTToGraph = DOTToGraph;
19889
19890
19891/***/ },
19892/* 40 */
19893/***/ function(module, exports, __webpack_require__) {
19894
19895
19896 function parseGephi(gephiJSON, options) {
19897 var edges = [];
19898 var nodes = [];
19899 this.options = {
19900 edges: {
19901 inheritColor: true
19902 },
19903 nodes: {
19904 allowedToMove: false,
19905 parseColor: false
19906 }
19907 };
19908
19909 if (options !== undefined) {
19910 this.options.nodes['allowedToMove'] = options.allowedToMove | false;
19911 this.options.nodes['parseColor'] = options.parseColor | false;
19912 this.options.edges['inheritColor'] = options.inheritColor | true;
19913 }
19914
19915 var gEdges = gephiJSON.edges;
19916 var gNodes = gephiJSON.nodes;
19917 for (var i = 0; i < gEdges.length; i++) {
19918 var edge = {};
19919 var gEdge = gEdges[i];
19920 edge['id'] = gEdge.id;
19921 edge['from'] = gEdge.source;
19922 edge['to'] = gEdge.target;
19923 edge['attributes'] = gEdge.attributes;
19924 // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
19925 // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
19926 edge['color'] = gEdge.color;
19927 edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor;
19928 edges.push(edge);
19929 }
19930
19931 for (var i = 0; i < gNodes.length; i++) {
19932 var node = {};
19933 var gNode = gNodes[i];
19934 node['id'] = gNode.id;
19935 node['attributes'] = gNode.attributes;
19936 node['x'] = gNode.x;
19937 node['y'] = gNode.y;
19938 node['label'] = gNode.label;
19939 if (this.options.nodes.parseColor == true) {
19940 node['color'] = gNode.color;
19941 }
19942 else {
19943 node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined;
19944 }
19945 node['radius'] = gNode.size;
19946 node['allowedToMoveX'] = this.options.nodes.allowedToMove;
19947 node['allowedToMoveY'] = this.options.nodes.allowedToMove;
19948 nodes.push(node);
19949 }
19950
19951 return {nodes:nodes, edges:edges};
19952 }
19953
19954 exports.parseGephi = parseGephi;
19955
19956/***/ },
19957/* 41 */
19958/***/ function(module, exports, __webpack_require__) {
19959
19960 // first check if moment.js is already loaded in the browser window, if so,
19961 // use this instance. Else, load via commonjs.
19962 module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(52);
19963
19964
19965/***/ },
19966/* 42 */
19967/***/ function(module, exports, __webpack_require__) {
19968
19969 // Only load hammer.js when in a browser environment
19970 // (loading hammer.js in a node.js environment gives errors)
19971 if (typeof window !== 'undefined') {
19972 module.exports = window['Hammer'] || __webpack_require__(53);
19973 }
19974 else {
19975 module.exports = function () {
19976 throw Error('hammer.js is only available in a browser, not in node.js.');
19977 }
19978 }
19979
19980
19981/***/ },
19982/* 43 */
19983/***/ function(module, exports, __webpack_require__) {
19984
19985 var Hammer = __webpack_require__(42);
19986
19987 /**
19988 * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
19989 * @param {Element} element
19990 * @param {Event} event
19991 */
19992 exports.fakeGesture = function(element, event) {
19993 var eventType = null;
19994
19995 // for hammer.js 1.0.5
19996 // var gesture = Hammer.event.collectEventData(this, eventType, event);
19997
19998 // for hammer.js 1.0.6+
19999 var touches = Hammer.event.getTouchList(event, eventType);
20000 var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
20001
20002 // on IE in standards mode, no touches are recognized by hammer.js,
20003 // resulting in NaN values for center.pageX and center.pageY
20004 if (isNaN(gesture.center.pageX)) {
20005 gesture.center.pageX = event.pageX;
20006 }
20007 if (isNaN(gesture.center.pageY)) {
20008 gesture.center.pageY = event.pageY;
20009 }
20010
20011 return gesture;
20012 };
20013
20014
20015/***/ },
20016/* 44 */
20017/***/ function(module, exports, __webpack_require__) {
20018
20019 var Emitter = __webpack_require__(50);
20020 var Hammer = __webpack_require__(42);
20021 var util = __webpack_require__(1);
20022 var DataSet = __webpack_require__(3);
20023 var DataView = __webpack_require__(4);
20024 var Range = __webpack_require__(15);
20025 var TimeAxis = __webpack_require__(27);
20026 var CurrentTime = __webpack_require__(19);
20027 var CustomTime = __webpack_require__(20);
20028 var ItemSet = __webpack_require__(24);
20029 var Activator = __webpack_require__(49);
20030
20031 /**
20032 * Create a timeline visualization
20033 * @param {HTMLElement} container
20034 * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
20035 * @param {Object} [options] See Core.setOptions for the available options.
20036 * @constructor
20037 */
20038 function Core () {}
20039
20040 // turn Core into an event emitter
20041 Emitter(Core.prototype);
20042
20043 /**
20044 * Create the main DOM for the Core: a root panel containing left, right,
20045 * top, bottom, content, and background panel.
20046 * @param {Element} container The container element where the Core will
20047 * be attached.
20048 * @private
20049 */
20050 Core.prototype._create = function (container) {
20051 this.dom = {};
20052
20053 this.dom.root = document.createElement('div');
20054 this.dom.background = document.createElement('div');
20055 this.dom.backgroundVertical = document.createElement('div');
20056 this.dom.backgroundHorizontal = document.createElement('div');
20057 this.dom.centerContainer = document.createElement('div');
20058 this.dom.leftContainer = document.createElement('div');
20059 this.dom.rightContainer = document.createElement('div');
20060 this.dom.center = document.createElement('div');
20061 this.dom.left = document.createElement('div');
20062 this.dom.right = document.createElement('div');
20063 this.dom.top = document.createElement('div');
20064 this.dom.bottom = document.createElement('div');
20065 this.dom.shadowTop = document.createElement('div');
20066 this.dom.shadowBottom = document.createElement('div');
20067 this.dom.shadowTopLeft = document.createElement('div');
20068 this.dom.shadowBottomLeft = document.createElement('div');
20069 this.dom.shadowTopRight = document.createElement('div');
20070 this.dom.shadowBottomRight = document.createElement('div');
20071
20072 this.dom.root.className = 'vis timeline root';
20073 this.dom.background.className = 'vispanel background';
20074 this.dom.backgroundVertical.className = 'vispanel background vertical';
20075 this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
20076 this.dom.centerContainer.className = 'vispanel center';
20077 this.dom.leftContainer.className = 'vispanel left';
20078 this.dom.rightContainer.className = 'vispanel right';
20079 this.dom.top.className = 'vispanel top';
20080 this.dom.bottom.className = 'vispanel bottom';
20081 this.dom.left.className = 'content';
20082 this.dom.center.className = 'content';
20083 this.dom.right.className = 'content';
20084 this.dom.shadowTop.className = 'shadow top';
20085 this.dom.shadowBottom.className = 'shadow bottom';
20086 this.dom.shadowTopLeft.className = 'shadow top';
20087 this.dom.shadowBottomLeft.className = 'shadow bottom';
20088 this.dom.shadowTopRight.className = 'shadow top';
20089 this.dom.shadowBottomRight.className = 'shadow bottom';
20090
20091 this.dom.root.appendChild(this.dom.background);
20092 this.dom.root.appendChild(this.dom.backgroundVertical);
20093 this.dom.root.appendChild(this.dom.backgroundHorizontal);
20094 this.dom.root.appendChild(this.dom.centerContainer);
20095 this.dom.root.appendChild(this.dom.leftContainer);
20096 this.dom.root.appendChild(this.dom.rightContainer);
20097 this.dom.root.appendChild(this.dom.top);
20098 this.dom.root.appendChild(this.dom.bottom);
20099
20100 this.dom.centerContainer.appendChild(this.dom.center);
20101 this.dom.leftContainer.appendChild(this.dom.left);
20102 this.dom.rightContainer.appendChild(this.dom.right);
20103
20104 this.dom.centerContainer.appendChild(this.dom.shadowTop);
20105 this.dom.centerContainer.appendChild(this.dom.shadowBottom);
20106 this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
20107 this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
20108 this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
20109 this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
20110
20111 this.on('rangechange', this.redraw.bind(this));
20112 this.on('change', this.redraw.bind(this));
20113 this.on('touch', this._onTouch.bind(this));
20114 this.on('pinch', this._onPinch.bind(this));
20115 this.on('dragstart', this._onDragStart.bind(this));
20116 this.on('drag', this._onDrag.bind(this));
20117
20118 // create event listeners for all interesting events, these events will be
20119 // emitted via emitter
20120 this.hammer = Hammer(this.dom.root, {
20121 preventDefault: true
20122 });
20123 this.listeners = {};
20124
20125 var me = this;
20126 var events = [
20127 'touch', 'pinch',
20128 'tap', 'doubletap', 'hold',
20129 'dragstart', 'drag', 'dragend',
20130 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
20131 ];
20132 events.forEach(function (event) {
20133 var listener = function () {
20134 var args = [event].concat(Array.prototype.slice.call(arguments, 0));
20135 if (me.isActive()) {
20136 me.emit.apply(me, args);
20137 }
20138 };
20139 me.hammer.on(event, listener);
20140 me.listeners[event] = listener;
20141 });
20142
20143 // size properties of each of the panels
20144 this.props = {
20145 root: {},
20146 background: {},
20147 centerContainer: {},
20148 leftContainer: {},
20149 rightContainer: {},
20150 center: {},
20151 left: {},
20152 right: {},
20153 top: {},
20154 bottom: {},
20155 border: {},
20156 scrollTop: 0,
20157 scrollTopMin: 0
20158 };
20159 this.touch = {}; // store state information needed for touch events
20160
20161 // attach the root panel to the provided container
20162 if (!container) throw new Error('No container provided');
20163 container.appendChild(this.dom.root);
20164 };
20165
20166 /**
20167 * Set options. Options will be passed to all components loaded in the Timeline.
20168 * @param {Object} [options]
20169 * {String} orientation
20170 * Vertical orientation for the Timeline,
20171 * can be 'bottom' (default) or 'top'.
20172 * {String | Number} width
20173 * Width for the timeline, a number in pixels or
20174 * a css string like '1000px' or '75%'. '100%' by default.
20175 * {String | Number} height
20176 * Fixed height for the Timeline, a number in pixels or
20177 * a css string like '400px' or '75%'. If undefined,
20178 * The Timeline will automatically size such that
20179 * its contents fit.
20180 * {String | Number} minHeight
20181 * Minimum height for the Timeline, a number in pixels or
20182 * a css string like '400px' or '75%'.
20183 * {String | Number} maxHeight
20184 * Maximum height for the Timeline, a number in pixels or
20185 * a css string like '400px' or '75%'.
20186 * {Number | Date | String} start
20187 * Start date for the visible window
20188 * {Number | Date | String} end
20189 * End date for the visible window
20190 */
20191 Core.prototype.setOptions = function (options) {
20192 if (options) {
20193 // copy the known options
20194 var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes'];
20195 util.selectiveExtend(fields, this.options, options);
20196
20197 if ('clickToUse' in options) {
20198 if (options.clickToUse) {
20199 this.activator = new Activator(this.dom.root);
20200 }
20201 else {
20202 if (this.activator) {
20203 this.activator.destroy();
20204 delete this.activator;
20205 }
20206 }
20207 }
20208
20209 // enable/disable autoResize
20210 this._initAutoResize();
20211 }
20212
20213 // propagate options to all components
20214 this.components.forEach(function (component) {
20215 component.setOptions(options);
20216 });
20217
20218 // TODO: remove deprecation error one day (deprecated since version 0.8.0)
20219 if (options && options.order) {
20220 throw new Error('Option order is deprecated. There is no replacement for this feature.');
20221 }
20222
20223 // redraw everything
20224 this.redraw();
20225 };
20226
20227 /**
20228 * Returns true when the Timeline is active.
20229 * @returns {boolean}
20230 */
20231 Core.prototype.isActive = function () {
20232 return !this.activator || this.activator.active;
20233 };
20234
20235 /**
20236 * Destroy the Core, clean up all DOM elements and event listeners.
20237 */
20238 Core.prototype.destroy = function () {
20239 // unbind datasets
20240 this.clear();
20241
20242 // remove all event listeners
20243 this.off();
20244
20245 // stop checking for changed size
20246 this._stopAutoResize();
20247
20248 // remove from DOM
20249 if (this.dom.root.parentNode) {
20250 this.dom.root.parentNode.removeChild(this.dom.root);
20251 }
20252 this.dom = null;
20253
20254 // remove Activator
20255 if (this.activator) {
20256 this.activator.destroy();
20257 delete this.activator;
20258 }
20259
20260 // cleanup hammer touch events
20261 for (var event in this.listeners) {
20262 if (this.listeners.hasOwnProperty(event)) {
20263 delete this.listeners[event];
20264 }
20265 }
20266 this.listeners = null;
20267 this.hammer = null;
20268
20269 // give all components the opportunity to cleanup
20270 this.components.forEach(function (component) {
20271 component.destroy();
20272 });
20273
20274 this.body = null;
20275 };
20276
20277
20278 /**
20279 * Set a custom time bar
20280 * @param {Date} time
20281 */
20282 Core.prototype.setCustomTime = function (time) {
20283 if (!this.customTime) {
20284 throw new Error('Cannot get custom time: Custom time bar is not enabled');
20285 }
20286
20287 this.customTime.setCustomTime(time);
20288 };
20289
20290 /**
20291 * Retrieve the current custom time.
20292 * @return {Date} customTime
20293 */
20294 Core.prototype.getCustomTime = function() {
20295 if (!this.customTime) {
20296 throw new Error('Cannot get custom time: Custom time bar is not enabled');
20297 }
20298
20299 return this.customTime.getCustomTime();
20300 };
20301
20302
20303 /**
20304 * Get the id's of the currently visible items.
20305 * @returns {Array} The ids of the visible items
20306 */
20307 Core.prototype.getVisibleItems = function() {
20308 return this.itemSet && this.itemSet.getVisibleItems() || [];
20309 };
20310
20311
20312
20313 /**
20314 * Clear the Core. By Default, items, groups and options are cleared.
20315 * Example usage:
20316 *
20317 * timeline.clear(); // clear items, groups, and options
20318 * timeline.clear({options: true}); // clear options only
20319 *
20320 * @param {Object} [what] Optionally specify what to clear. By default:
20321 * {items: true, groups: true, options: true}
20322 */
20323 Core.prototype.clear = function(what) {
20324 // clear items
20325 if (!what || what.items) {
20326 this.setItems(null);
20327 }
20328
20329 // clear groups
20330 if (!what || what.groups) {
20331 this.setGroups(null);
20332 }
20333
20334 // clear options of timeline and of each of the components
20335 if (!what || what.options) {
20336 this.components.forEach(function (component) {
20337 component.setOptions(component.defaultOptions);
20338 });
20339
20340 this.setOptions(this.defaultOptions); // this will also do a redraw
20341 }
20342 };
20343
20344 /**
20345 * Set Core window such that it fits all items
20346 * @param {Object} [options] Available options:
20347 * `animate: boolean | number`
20348 * If true (default), the range is animated
20349 * smoothly to the new window.
20350 * If a number, the number is taken as duration
20351 * for the animation. Default duration is 500 ms.
20352 */
20353 Core.prototype.fit = function(options) {
20354 // apply the data range as range
20355 var dataRange = this.getItemRange();
20356
20357 // add 5% space on both sides
20358 var start = dataRange.min;
20359 var end = dataRange.max;
20360 if (start != null && end != null) {
20361 var interval = (end.valueOf() - start.valueOf());
20362 if (interval <= 0) {
20363 // prevent an empty interval
20364 interval = 24 * 60 * 60 * 1000; // 1 day
20365 }
20366 start = new Date(start.valueOf() - interval * 0.05);
20367 end = new Date(end.valueOf() + interval * 0.05);
20368 }
20369
20370 // skip range set if there is no start and end date
20371 if (start === null && end === null) {
20372 return;
20373 }
20374
20375 var animate = (options && options.animate !== undefined) ? options.animate : true;
20376 this.range.setRange(start, end, animate);
20377 };
20378
20379 /**
20380 * Set the visible window. Both parameters are optional, you can change only
20381 * start or only end. Syntax:
20382 *
20383 * TimeLine.setWindow(start, end)
20384 * TimeLine.setWindow(range)
20385 *
20386 * Where start and end can be a Date, number, or string, and range is an
20387 * object with properties start and end.
20388 *
20389 * @param {Date | Number | String | Object} [start] Start date of visible window
20390 * @param {Date | Number | String} [end] End date of visible window
20391 * @param {Object} [options] Available options:
20392 * `animate: boolean | number`
20393 * If true (default), the range is animated
20394 * smoothly to the new window.
20395 * If a number, the number is taken as duration
20396 * for the animation. Default duration is 500 ms.
20397 */
20398 Core.prototype.setWindow = function(start, end, options) {
20399 var animate = (options && options.animate !== undefined) ? options.animate : true;
20400 if (arguments.length == 1) {
20401 var range = arguments[0];
20402 this.range.setRange(range.start, range.end, animate);
20403 }
20404 else {
20405 this.range.setRange(start, end, animate);
20406 }
20407 };
20408
20409 /**
20410 * Move the window such that given time is centered on screen.
20411 * @param {Date | Number | String} time
20412 * @param {Object} [options] Available options:
20413 * `animate: boolean | number`
20414 * If true (default), the range is animated
20415 * smoothly to the new window.
20416 * If a number, the number is taken as duration
20417 * for the animation. Default duration is 500 ms.
20418 */
20419 Core.prototype.moveTo = function(time, options) {
20420 var interval = this.range.end - this.range.start;
20421 var t = util.convert(time, 'Date').valueOf();
20422
20423 var start = t - interval / 2;
20424 var end = t + interval / 2;
20425 var animate = (options && options.animate !== undefined) ? options.animate : true;
20426
20427 this.range.setRange(start, end, animate);
20428 };
20429
20430 /**
20431 * Get the visible window
20432 * @return {{start: Date, end: Date}} Visible range
20433 */
20434 Core.prototype.getWindow = function() {
20435 var range = this.range.getRange();
20436 return {
20437 start: new Date(range.start),
20438 end: new Date(range.end)
20439 };
20440 };
20441
20442 /**
20443 * Force a redraw of the Core. Can be useful to manually redraw when
20444 * option autoResize=false
20445 */
20446 Core.prototype.redraw = function() {
20447 var resized = false,
20448 options = this.options,
20449 props = this.props,
20450 dom = this.dom;
20451
20452 if (!dom) return; // when destroyed
20453
20454 // update class names
20455 if (options.orientation == 'top') {
20456 util.addClassName(dom.root, 'top');
20457 util.removeClassName(dom.root, 'bottom');
20458 }
20459 else {
20460 util.removeClassName(dom.root, 'top');
20461 util.addClassName(dom.root, 'bottom');
20462 }
20463
20464 // update root width and height options
20465 dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
20466 dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
20467 dom.root.style.width = util.option.asSize(options.width, '');
20468
20469 // calculate border widths
20470 props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
20471 props.border.right = props.border.left;
20472 props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
20473 props.border.bottom = props.border.top;
20474 var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
20475 var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
20476
20477 // workaround for a bug in IE: the clientWidth of an element with
20478 // a height:0px and overflow:hidden is not calculated and always has value 0
20479 if (dom.centerContainer.clientHeight === 0) {
20480 props.border.left = props.border.top;
20481 props.border.right = props.border.left;
20482 }
20483 if (dom.root.clientHeight === 0) {
20484 borderRootWidth = borderRootHeight;
20485 }
20486
20487 // calculate the heights. If any of the side panels is empty, we set the height to
20488 // minus the border width, such that the border will be invisible
20489 props.center.height = dom.center.offsetHeight;
20490 props.left.height = dom.left.offsetHeight;
20491 props.right.height = dom.right.offsetHeight;
20492 props.top.height = dom.top.clientHeight || -props.border.top;
20493 props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
20494
20495 // TODO: compensate borders when any of the panels is empty.
20496
20497 // apply auto height
20498 // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
20499 var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
20500 var autoHeight = props.top.height + contentHeight + props.bottom.height +
20501 borderRootHeight + props.border.top + props.border.bottom;
20502 dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
20503
20504 // calculate heights of the content panels
20505 props.root.height = dom.root.offsetHeight;
20506 props.background.height = props.root.height - borderRootHeight;
20507 var containerHeight = props.root.height - props.top.height - props.bottom.height -
20508 borderRootHeight;
20509 props.centerContainer.height = containerHeight;
20510 props.leftContainer.height = containerHeight;
20511 props.rightContainer.height = props.leftContainer.height;
20512
20513 // calculate the widths of the panels
20514 props.root.width = dom.root.offsetWidth;
20515 props.background.width = props.root.width - borderRootWidth;
20516 props.left.width = dom.leftContainer.clientWidth || -props.border.left;
20517 props.leftContainer.width = props.left.width;
20518 props.right.width = dom.rightContainer.clientWidth || -props.border.right;
20519 props.rightContainer.width = props.right.width;
20520 var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
20521 props.center.width = centerWidth;
20522 props.centerContainer.width = centerWidth;
20523 props.top.width = centerWidth;
20524 props.bottom.width = centerWidth;
20525
20526 // resize the panels
20527 dom.background.style.height = props.background.height + 'px';
20528 dom.backgroundVertical.style.height = props.background.height + 'px';
20529 dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
20530 dom.centerContainer.style.height = props.centerContainer.height + 'px';
20531 dom.leftContainer.style.height = props.leftContainer.height + 'px';
20532 dom.rightContainer.style.height = props.rightContainer.height + 'px';
20533
20534 dom.background.style.width = props.background.width + 'px';
20535 dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
20536 dom.backgroundHorizontal.style.width = props.background.width + 'px';
20537 dom.centerContainer.style.width = props.center.width + 'px';
20538 dom.top.style.width = props.top.width + 'px';
20539 dom.bottom.style.width = props.bottom.width + 'px';
20540
20541 // reposition the panels
20542 dom.background.style.left = '0';
20543 dom.background.style.top = '0';
20544 dom.backgroundVertical.style.left = (props.left.width + props.border.left) + 'px';
20545 dom.backgroundVertical.style.top = '0';
20546 dom.backgroundHorizontal.style.left = '0';
20547 dom.backgroundHorizontal.style.top = props.top.height + 'px';
20548 dom.centerContainer.style.left = props.left.width + 'px';
20549 dom.centerContainer.style.top = props.top.height + 'px';
20550 dom.leftContainer.style.left = '0';
20551 dom.leftContainer.style.top = props.top.height + 'px';
20552 dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
20553 dom.rightContainer.style.top = props.top.height + 'px';
20554 dom.top.style.left = props.left.width + 'px';
20555 dom.top.style.top = '0';
20556 dom.bottom.style.left = props.left.width + 'px';
20557 dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
20558
20559 // update the scrollTop, feasible range for the offset can be changed
20560 // when the height of the Core or of the contents of the center changed
20561 this._updateScrollTop();
20562
20563 // reposition the scrollable contents
20564 var offset = this.props.scrollTop;
20565 if (options.orientation == 'bottom') {
20566 offset += Math.max(this.props.centerContainer.height - this.props.center.height -
20567 this.props.border.top - this.props.border.bottom, 0);
20568 }
20569 dom.center.style.left = '0';
20570 dom.center.style.top = offset + 'px';
20571 dom.left.style.left = '0';
20572 dom.left.style.top = offset + 'px';
20573 dom.right.style.left = '0';
20574 dom.right.style.top = offset + 'px';
20575
20576 // show shadows when vertical scrolling is available
20577 var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
20578 var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
20579 dom.shadowTop.style.visibility = visibilityTop;
20580 dom.shadowBottom.style.visibility = visibilityBottom;
20581 dom.shadowTopLeft.style.visibility = visibilityTop;
20582 dom.shadowBottomLeft.style.visibility = visibilityBottom;
20583 dom.shadowTopRight.style.visibility = visibilityTop;
20584 dom.shadowBottomRight.style.visibility = visibilityBottom;
20585
20586 // redraw all components
20587 this.components.forEach(function (component) {
20588 resized = component.redraw() || resized;
20589 });
20590 if (resized) {
20591 // keep repainting until all sizes are settled
20592 this.redraw();
20593 }
20594 };
20595
20596 // TODO: deprecated since version 1.1.0, remove some day
20597 Core.prototype.repaint = function () {
20598 throw new Error('Function repaint is deprecated. Use redraw instead.');
20599 };
20600
20601 /**
20602 * Set a current time. This can be used for example to ensure that a client's
20603 * time is synchronized with a shared server time.
20604 * Only applicable when option `showCurrentTime` is true.
20605 * @param {Date | String | Number} time A Date, unix timestamp, or
20606 * ISO date string.
20607 */
20608 Core.prototype.setCurrentTime = function(time) {
20609 if (!this.currentTime) {
20610 throw new Error('Option showCurrentTime must be true');
20611 }
20612
20613 this.currentTime.setCurrentTime(time);
20614 };
20615
20616 /**
20617 * Get the current time.
20618 * Only applicable when option `showCurrentTime` is true.
20619 * @return {Date} Returns the current time.
20620 */
20621 Core.prototype.getCurrentTime = function() {
20622 if (!this.currentTime) {
20623 throw new Error('Option showCurrentTime must be true');
20624 }
20625
20626 return this.currentTime.getCurrentTime();
20627 };
20628
20629 /**
20630 * Convert a position on screen (pixels) to a datetime
20631 * @param {int} x Position on the screen in pixels
20632 * @return {Date} time The datetime the corresponds with given position x
20633 * @private
20634 */
20635 // TODO: move this function to Range
20636 Core.prototype._toTime = function(x) {
20637 var conversion = this.range.conversion(this.props.center.width);
20638 return new Date(x / conversion.scale + conversion.offset);
20639 };
20640
20641
20642 /**
20643 * Convert a position on the global screen (pixels) to a datetime
20644 * @param {int} x Position on the screen in pixels
20645 * @return {Date} time The datetime the corresponds with given position x
20646 * @private
20647 */
20648 // TODO: move this function to Range
20649 Core.prototype._toGlobalTime = function(x) {
20650 var conversion = this.range.conversion(this.props.root.width);
20651 return new Date(x / conversion.scale + conversion.offset);
20652 };
20653
20654 /**
20655 * Convert a datetime (Date object) into a position on the screen
20656 * @param {Date} time A date
20657 * @return {int} x The position on the screen in pixels which corresponds
20658 * with the given date.
20659 * @private
20660 */
20661 // TODO: move this function to Range
20662 Core.prototype._toScreen = function(time) {
20663 var conversion = this.range.conversion(this.props.center.width);
20664 return (time.valueOf() - conversion.offset) * conversion.scale;
20665 };
20666
20667
20668 /**
20669 * Convert a datetime (Date object) into a position on the root
20670 * This is used to get the pixel density estimate for the screen, not the center panel
20671 * @param {Date} time A date
20672 * @return {int} x The position on root in pixels which corresponds
20673 * with the given date.
20674 * @private
20675 */
20676 // TODO: move this function to Range
20677 Core.prototype._toGlobalScreen = function(time) {
20678 var conversion = this.range.conversion(this.props.root.width);
20679 return (time.valueOf() - conversion.offset) * conversion.scale;
20680 };
20681
20682
20683 /**
20684 * Initialize watching when option autoResize is true
20685 * @private
20686 */
20687 Core.prototype._initAutoResize = function () {
20688 if (this.options.autoResize == true) {
20689 this._startAutoResize();
20690 }
20691 else {
20692 this._stopAutoResize();
20693 }
20694 };
20695
20696 /**
20697 * Watch for changes in the size of the container. On resize, the Panel will
20698 * automatically redraw itself.
20699 * @private
20700 */
20701 Core.prototype._startAutoResize = function () {
20702 var me = this;
20703
20704 this._stopAutoResize();
20705
20706 this._onResize = function() {
20707 if (me.options.autoResize != true) {
20708 // stop watching when the option autoResize is changed to false
20709 me._stopAutoResize();
20710 return;
20711 }
20712
20713 if (me.dom.root) {
20714 // check whether the frame is resized
20715 // Note: we compare offsetWidth here, not clientWidth. For some reason,
20716 // IE does not restore the clientWidth from 0 to the actual width after
20717 // changing the timeline's container display style from none to visible
20718 if ((me.dom.root.offsetWidth != me.props.lastWidth) ||
20719 (me.dom.root.offsetHeight != me.props.lastHeight)) {
20720 me.props.lastWidth = me.dom.root.offsetWidth;
20721 me.props.lastHeight = me.dom.root.offsetHeight;
20722
20723 me.emit('change');
20724 }
20725 }
20726 };
20727
20728 // add event listener to window resize
20729 util.addEventListener(window, 'resize', this._onResize);
20730
20731 this.watchTimer = setInterval(this._onResize, 1000);
20732 };
20733
20734 /**
20735 * Stop watching for a resize of the frame.
20736 * @private
20737 */
20738 Core.prototype._stopAutoResize = function () {
20739 if (this.watchTimer) {
20740 clearInterval(this.watchTimer);
20741 this.watchTimer = undefined;
20742 }
20743
20744 // remove event listener on window.resize
20745 util.removeEventListener(window, 'resize', this._onResize);
20746 this._onResize = null;
20747 };
20748
20749 /**
20750 * Start moving the timeline vertically
20751 * @param {Event} event
20752 * @private
20753 */
20754 Core.prototype._onTouch = function (event) {
20755 this.touch.allowDragging = true;
20756 };
20757
20758 /**
20759 * Start moving the timeline vertically
20760 * @param {Event} event
20761 * @private
20762 */
20763 Core.prototype._onPinch = function (event) {
20764 this.touch.allowDragging = false;
20765 };
20766
20767 /**
20768 * Start moving the timeline vertically
20769 * @param {Event} event
20770 * @private
20771 */
20772 Core.prototype._onDragStart = function (event) {
20773 this.touch.initialScrollTop = this.props.scrollTop;
20774 };
20775
20776 /**
20777 * Move the timeline vertically
20778 * @param {Event} event
20779 * @private
20780 */
20781 Core.prototype._onDrag = function (event) {
20782 // refuse to drag when we where pinching to prevent the timeline make a jump
20783 // when releasing the fingers in opposite order from the touch screen
20784 if (!this.touch.allowDragging) return;
20785
20786 var delta = event.gesture.deltaY;
20787
20788 var oldScrollTop = this._getScrollTop();
20789 var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
20790
20791 if (newScrollTop != oldScrollTop) {
20792 this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
20793 }
20794 };
20795
20796 /**
20797 * Apply a scrollTop
20798 * @param {Number} scrollTop
20799 * @returns {Number} scrollTop Returns the applied scrollTop
20800 * @private
20801 */
20802 Core.prototype._setScrollTop = function (scrollTop) {
20803 this.props.scrollTop = scrollTop;
20804 this._updateScrollTop();
20805 return this.props.scrollTop;
20806 };
20807
20808 /**
20809 * Update the current scrollTop when the height of the containers has been changed
20810 * @returns {Number} scrollTop Returns the applied scrollTop
20811 * @private
20812 */
20813 Core.prototype._updateScrollTop = function () {
20814 // recalculate the scrollTopMin
20815 var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
20816 if (scrollTopMin != this.props.scrollTopMin) {
20817 // in case of bottom orientation, change the scrollTop such that the contents
20818 // do not move relative to the time axis at the bottom
20819 if (this.options.orientation == 'bottom') {
20820 this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
20821 }
20822 this.props.scrollTopMin = scrollTopMin;
20823 }
20824
20825 // limit the scrollTop to the feasible scroll range
20826 if (this.props.scrollTop > 0) this.props.scrollTop = 0;
20827 if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
20828
20829 return this.props.scrollTop;
20830 };
20831
20832 /**
20833 * Get the current scrollTop
20834 * @returns {number} scrollTop
20835 * @private
20836 */
20837 Core.prototype._getScrollTop = function () {
20838 return this.props.scrollTop;
20839 };
20840
20841 module.exports = Core;
20842
20843
20844/***/ },
20845/* 45 */
20846/***/ function(module, exports, __webpack_require__) {
20847
20848 // English
20849 exports['en'] = {
20850 current: 'current',
20851 time: 'time'
20852 };
20853 exports['en_EN'] = exports['en'];
20854 exports['en_US'] = exports['en'];
20855
20856 // Dutch
20857 exports['nl'] = {
20858 custom: 'aangepaste',
20859 time: 'tijd'
20860 };
20861 exports['nl_NL'] = exports['nl'];
20862 exports['nl_BE'] = exports['nl'];
20863
20864
20865/***/ },
20866/* 46 */
20867/***/ function(module, exports, __webpack_require__) {
20868
20869 // English
20870 exports['en'] = {
20871 edit: 'Edit',
20872 del: 'Delete selected',
20873 back: 'Back',
20874 addNode: 'Add Node',
20875 addEdge: 'Add Edge',
20876 editNode: 'Edit Node',
20877 editEdge: 'Edit Edge',
20878 addDescription: 'Click in an empty space to place a new node.',
20879 edgeDescription: 'Click on a node and drag the edge to another node to connect them.',
20880 editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.',
20881 createEdgeError: 'Cannot link edges to a cluster.',
20882 deleteClusterError: 'Clusters cannot be deleted.'
20883 };
20884 exports['en_EN'] = exports['en'];
20885 exports['en_US'] = exports['en'];
20886
20887 // Dutch
20888 exports['nl'] = {
20889 edit: 'Wijzigen',
20890 del: 'Selectie verwijderen',
20891 back: 'Terug',
20892 addNode: 'Node toevoegen',
20893 addEdge: 'Link toevoegen',
20894 editNode: 'Node wijzigen',
20895 editEdge: 'Link wijzigen',
20896 addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.',
20897 edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.',
20898 editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.',
20899 createEdgeError: 'Kan geen link maken naar een cluster.',
20900 deleteClusterError: 'Clusters kunnen niet worden verwijderd.'
20901 };
20902 exports['nl_NL'] = exports['nl'];
20903 exports['nl_BE'] = exports['nl'];
20904
20905
20906/***/ },
20907/* 47 */
20908/***/ function(module, exports, __webpack_require__) {
20909
20910 /**
20911 * Canvas shapes used by Network
20912 */
20913 if (typeof CanvasRenderingContext2D !== 'undefined') {
20914
20915 /**
20916 * Draw a circle shape
20917 */
20918 CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
20919 this.beginPath();
20920 this.arc(x, y, r, 0, 2*Math.PI, false);
20921 };
20922
20923 /**
20924 * Draw a square shape
20925 * @param {Number} x horizontal center
20926 * @param {Number} y vertical center
20927 * @param {Number} r size, width and height of the square
20928 */
20929 CanvasRenderingContext2D.prototype.square = function(x, y, r) {
20930 this.beginPath();
20931 this.rect(x - r, y - r, r * 2, r * 2);
20932 };
20933
20934 /**
20935 * Draw a triangle shape
20936 * @param {Number} x horizontal center
20937 * @param {Number} y vertical center
20938 * @param {Number} r radius, half the length of the sides of the triangle
20939 */
20940 CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
20941 // http://en.wikipedia.org/wiki/Equilateral_triangle
20942 this.beginPath();
20943
20944 var s = r * 2;
20945 var s2 = s / 2;
20946 var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
20947 var h = Math.sqrt(s * s - s2 * s2); // height
20948
20949 this.moveTo(x, y - (h - ir));
20950 this.lineTo(x + s2, y + ir);
20951 this.lineTo(x - s2, y + ir);
20952 this.lineTo(x, y - (h - ir));
20953 this.closePath();
20954 };
20955
20956 /**
20957 * Draw a triangle shape in downward orientation
20958 * @param {Number} x horizontal center
20959 * @param {Number} y vertical center
20960 * @param {Number} r radius
20961 */
20962 CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
20963 // http://en.wikipedia.org/wiki/Equilateral_triangle
20964 this.beginPath();
20965
20966 var s = r * 2;
20967 var s2 = s / 2;
20968 var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
20969 var h = Math.sqrt(s * s - s2 * s2); // height
20970
20971 this.moveTo(x, y + (h - ir));
20972 this.lineTo(x + s2, y - ir);
20973 this.lineTo(x - s2, y - ir);
20974 this.lineTo(x, y + (h - ir));
20975 this.closePath();
20976 };
20977
20978 /**
20979 * Draw a star shape, a star with 5 points
20980 * @param {Number} x horizontal center
20981 * @param {Number} y vertical center
20982 * @param {Number} r radius, half the length of the sides of the triangle
20983 */
20984 CanvasRenderingContext2D.prototype.star = function(x, y, r) {
20985 // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
20986 this.beginPath();
20987
20988 for (var n = 0; n < 10; n++) {
20989 var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
20990 this.lineTo(
20991 x + radius * Math.sin(n * 2 * Math.PI / 10),
20992 y - radius * Math.cos(n * 2 * Math.PI / 10)
20993 );
20994 }
20995
20996 this.closePath();
20997 };
20998
20999 /**
21000 * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
21001 */
21002 CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
21003 var r2d = Math.PI/180;
21004 if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
21005 if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
21006 this.beginPath();
21007 this.moveTo(x+r,y);
21008 this.lineTo(x+w-r,y);
21009 this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
21010 this.lineTo(x+w,y+h-r);
21011 this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
21012 this.lineTo(x+r,y+h);
21013 this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
21014 this.lineTo(x,y+r);
21015 this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
21016 };
21017
21018 /**
21019 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
21020 */
21021 CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
21022 var kappa = .5522848,
21023 ox = (w / 2) * kappa, // control point offset horizontal
21024 oy = (h / 2) * kappa, // control point offset vertical
21025 xe = x + w, // x-end
21026 ye = y + h, // y-end
21027 xm = x + w / 2, // x-middle
21028 ym = y + h / 2; // y-middle
21029
21030 this.beginPath();
21031 this.moveTo(x, ym);
21032 this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
21033 this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
21034 this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
21035 this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
21036 };
21037
21038
21039
21040 /**
21041 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
21042 */
21043 CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
21044 var f = 1/3;
21045 var wEllipse = w;
21046 var hEllipse = h * f;
21047
21048 var kappa = .5522848,
21049 ox = (wEllipse / 2) * kappa, // control point offset horizontal
21050 oy = (hEllipse / 2) * kappa, // control point offset vertical
21051 xe = x + wEllipse, // x-end
21052 ye = y + hEllipse, // y-end
21053 xm = x + wEllipse / 2, // x-middle
21054 ym = y + hEllipse / 2, // y-middle
21055 ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
21056 yeb = y + h; // y-end, bottom ellipse
21057
21058 this.beginPath();
21059 this.moveTo(xe, ym);
21060
21061 this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
21062 this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
21063
21064 this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
21065 this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
21066
21067 this.lineTo(xe, ymb);
21068
21069 this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
21070 this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
21071
21072 this.lineTo(x, ym);
21073 };
21074
21075
21076 /**
21077 * Draw an arrow point (no line)
21078 */
21079 CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
21080 // tail
21081 var xt = x - length * Math.cos(angle);
21082 var yt = y - length * Math.sin(angle);
21083
21084 // inner tail
21085 // TODO: allow to customize different shapes
21086 var xi = x - length * 0.9 * Math.cos(angle);
21087 var yi = y - length * 0.9 * Math.sin(angle);
21088
21089 // left
21090 var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
21091 var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
21092
21093 // right
21094 var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
21095 var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
21096
21097 this.beginPath();
21098 this.moveTo(x, y);
21099 this.lineTo(xl, yl);
21100 this.lineTo(xi, yi);
21101 this.lineTo(xr, yr);
21102 this.closePath();
21103 };
21104
21105 /**
21106 * Sets up the dashedLine functionality for drawing
21107 * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
21108 * @author David Jordan
21109 * @date 2012-08-08
21110 */
21111 CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
21112 if (!dashArray) dashArray=[10,5];
21113 if (dashLength==0) dashLength = 0.001; // Hack for Safari
21114 var dashCount = dashArray.length;
21115 this.moveTo(x, y);
21116 var dx = (x2-x), dy = (y2-y);
21117 var slope = dy/dx;
21118 var distRemaining = Math.sqrt( dx*dx + dy*dy );
21119 var dashIndex=0, draw=true;
21120 while (distRemaining>=0.1){
21121 var dashLength = dashArray[dashIndex++%dashCount];
21122 if (dashLength > distRemaining) dashLength = distRemaining;
21123 var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
21124 if (dx<0) xStep = -xStep;
21125 x += xStep;
21126 y += slope*xStep;
21127 this[draw ? 'lineTo' : 'moveTo'](x,y);
21128 distRemaining -= dashLength;
21129 draw = !draw;
21130 }
21131 };
21132
21133 // TODO: add diamond shape
21134 }
21135
21136
21137/***/ },
21138/* 48 */
21139/***/ function(module, exports, __webpack_require__) {
21140
21141 var PhysicsMixin = __webpack_require__(60);
21142 var ClusterMixin = __webpack_require__(54);
21143 var SectorsMixin = __webpack_require__(55);
21144 var SelectionMixin = __webpack_require__(56);
21145 var ManipulationMixin = __webpack_require__(57);
21146 var NavigationMixin = __webpack_require__(58);
21147 var HierarchicalLayoutMixin = __webpack_require__(59);
21148
21149 /**
21150 * Load a mixin into the network object
21151 *
21152 * @param {Object} sourceVariable | this object has to contain functions.
21153 * @private
21154 */
21155 exports._loadMixin = function (sourceVariable) {
21156 for (var mixinFunction in sourceVariable) {
21157 if (sourceVariable.hasOwnProperty(mixinFunction)) {
21158 this[mixinFunction] = sourceVariable[mixinFunction];
21159 }
21160 }
21161 };
21162
21163
21164 /**
21165 * removes a mixin from the network object.
21166 *
21167 * @param {Object} sourceVariable | this object has to contain functions.
21168 * @private
21169 */
21170 exports._clearMixin = function (sourceVariable) {
21171 for (var mixinFunction in sourceVariable) {
21172 if (sourceVariable.hasOwnProperty(mixinFunction)) {
21173 this[mixinFunction] = undefined;
21174 }
21175 }
21176 };
21177
21178
21179 /**
21180 * Mixin the physics system and initialize the parameters required.
21181 *
21182 * @private
21183 */
21184 exports._loadPhysicsSystem = function () {
21185 this._loadMixin(PhysicsMixin);
21186 this._loadSelectedForceSolver();
21187 if (this.constants.configurePhysics == true) {
21188 this._loadPhysicsConfiguration();
21189 }
21190 };
21191
21192
21193 /**
21194 * Mixin the cluster system and initialize the parameters required.
21195 *
21196 * @private
21197 */
21198 exports._loadClusterSystem = function () {
21199 this.clusterSession = 0;
21200 this.hubThreshold = 5;
21201 this._loadMixin(ClusterMixin);
21202 };
21203
21204
21205 /**
21206 * Mixin the sector system and initialize the parameters required
21207 *
21208 * @private
21209 */
21210 exports._loadSectorSystem = function () {
21211 this.sectors = {};
21212 this.activeSector = ["default"];
21213 this.sectors["active"] = {};
21214 this.sectors["active"]["default"] = {"nodes": {},
21215 "edges": {},
21216 "nodeIndices": [],
21217 "formationScale": 1.0,
21218 "drawingNode": undefined };
21219 this.sectors["frozen"] = {};
21220 this.sectors["support"] = {"nodes": {},
21221 "edges": {},
21222 "nodeIndices": [],
21223 "formationScale": 1.0,
21224 "drawingNode": undefined };
21225
21226 this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
21227
21228 this._loadMixin(SectorsMixin);
21229 };
21230
21231
21232 /**
21233 * Mixin the selection system and initialize the parameters required
21234 *
21235 * @private
21236 */
21237 exports._loadSelectionSystem = function () {
21238 this.selectionObj = {nodes: {}, edges: {}};
21239
21240 this._loadMixin(SelectionMixin);
21241 };
21242
21243
21244 /**
21245 * Mixin the navigationUI (User Interface) system and initialize the parameters required
21246 *
21247 * @private
21248 */
21249 exports._loadManipulationSystem = function () {
21250 // reset global variables -- these are used by the selection of nodes and edges.
21251 this.blockConnectingEdgeSelection = false;
21252 this.forceAppendSelection = false;
21253
21254 if (this.constants.dataManipulation.enabled == true) {
21255 // load the manipulator HTML elements. All styling done in css.
21256 if (this.manipulationDiv === undefined) {
21257 this.manipulationDiv = document.createElement('div');
21258 this.manipulationDiv.className = 'network-manipulationDiv';
21259 this.manipulationDiv.id = 'network-manipulationDiv';
21260 if (this.editMode == true) {
21261 this.manipulationDiv.style.display = "block";
21262 }
21263 else {
21264 this.manipulationDiv.style.display = "none";
21265 }
21266 this.frame.appendChild(this.manipulationDiv);
21267 }
21268
21269 if (this.editModeDiv === undefined) {
21270 this.editModeDiv = document.createElement('div');
21271 this.editModeDiv.className = 'network-manipulation-editMode';
21272 this.editModeDiv.id = 'network-manipulation-editMode';
21273 if (this.editMode == true) {
21274 this.editModeDiv.style.display = "none";
21275 }
21276 else {
21277 this.editModeDiv.style.display = "block";
21278 }
21279 this.frame.appendChild(this.editModeDiv);
21280 }
21281
21282 if (this.closeDiv === undefined) {
21283 this.closeDiv = document.createElement('div');
21284 this.closeDiv.className = 'network-manipulation-closeDiv';
21285 this.closeDiv.id = 'network-manipulation-closeDiv';
21286 this.closeDiv.style.display = this.manipulationDiv.style.display;
21287 this.frame.appendChild(this.closeDiv);
21288 }
21289
21290 // load the manipulation functions
21291 this._loadMixin(ManipulationMixin);
21292
21293 // create the manipulator toolbar
21294 this._createManipulatorBar();
21295 }
21296 else {
21297 if (this.manipulationDiv !== undefined) {
21298 // removes all the bindings and overloads
21299 this._createManipulatorBar();
21300
21301 // remove the manipulation divs
21302 this.frame.removeChild(this.manipulationDiv);
21303 this.frame.removeChild(this.editModeDiv);
21304 this.frame.removeChild(this.closeDiv);
21305
21306 this.manipulationDiv = undefined;
21307 this.editModeDiv = undefined;
21308 this.closeDiv = undefined;
21309 // remove the mixin functions
21310 this._clearMixin(ManipulationMixin);
21311 }
21312 }
21313 };
21314
21315
21316 /**
21317 * Mixin the navigation (User Interface) system and initialize the parameters required
21318 *
21319 * @private
21320 */
21321 exports._loadNavigationControls = function () {
21322 this._loadMixin(NavigationMixin);
21323 // the clean function removes the button divs, this is done to remove the bindings.
21324 this._cleanNavigation();
21325 if (this.constants.navigation.enabled == true) {
21326 this._loadNavigationElements();
21327 }
21328 };
21329
21330
21331 /**
21332 * Mixin the hierarchical layout system.
21333 *
21334 * @private
21335 */
21336 exports._loadHierarchySystem = function () {
21337 this._loadMixin(HierarchicalLayoutMixin);
21338 };
21339
21340
21341/***/ },
21342/* 49 */
21343/***/ function(module, exports, __webpack_require__) {
21344
21345 var mousetrap = __webpack_require__(51);
21346 var Emitter = __webpack_require__(50);
21347 var Hammer = __webpack_require__(42);
21348 var util = __webpack_require__(1);
21349
21350 /**
21351 * Turn an element into an clickToUse element.
21352 * When not active, the element has a transparent overlay. When the overlay is
21353 * clicked, the mode is changed to active.
21354 * When active, the element is displayed with a blue border around it, and
21355 * the interactive contents of the element can be used. When clicked outside
21356 * the element, the elements mode is changed to inactive.
21357 * @param {Element} container
21358 * @constructor
21359 */
21360 function Activator(container) {
21361 this.active = false;
21362
21363 this.dom = {
21364 container: container
21365 };
21366
21367 this.dom.overlay = document.createElement('div');
21368 this.dom.overlay.className = 'overlay';
21369
21370 this.dom.container.appendChild(this.dom.overlay);
21371
21372 this.hammer = Hammer(this.dom.overlay, {prevent_default: false});
21373 this.hammer.on('tap', this._onTapOverlay.bind(this));
21374
21375 // block all touch events (except tap)
21376 var me = this;
21377 var events = [
21378 'touch', 'pinch',
21379 'doubletap', 'hold',
21380 'dragstart', 'drag', 'dragend',
21381 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
21382 ];
21383 events.forEach(function (event) {
21384 me.hammer.on(event, function (event) {
21385 event.stopPropagation();
21386 });
21387 });
21388
21389 // attach a tap event to the window, in order to deactivate when clicking outside the timeline
21390 this.windowHammer = Hammer(window, {prevent_default: false});
21391 this.windowHammer.on('tap', function (event) {
21392 // deactivate when clicked outside the container
21393 if (!_hasParent(event.target, container)) {
21394 me.deactivate();
21395 }
21396 });
21397
21398 // mousetrap listener only bounded when active)
21399 this.escListener = this.deactivate.bind(this);
21400 }
21401
21402 // turn into an event emitter
21403 Emitter(Activator.prototype);
21404
21405 // The currently active activator
21406 Activator.current = null;
21407
21408 /**
21409 * Destroy the activator. Cleans up all created DOM and event listeners
21410 */
21411 Activator.prototype.destroy = function () {
21412 this.deactivate();
21413
21414 // remove dom
21415 this.dom.overlay.parentNode.removeChild(this.dom.overlay);
21416
21417 // cleanup hammer instances
21418 this.hammer = null;
21419 this.windowHammer = null;
21420 // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory)
21421 };
21422
21423 /**
21424 * Activate the element
21425 * Overlay is hidden, element is decorated with a blue shadow border
21426 */
21427 Activator.prototype.activate = function () {
21428 // we allow only one active activator at a time
21429 if (Activator.current) {
21430 Activator.current.deactivate();
21431 }
21432 Activator.current = this;
21433
21434 this.active = true;
21435 this.dom.overlay.style.display = 'none';
21436 util.addClassName(this.dom.container, 'vis-active');
21437
21438 this.emit('change');
21439 this.emit('activate');
21440
21441 // ugly hack: bind ESC after emitting the events, as the Network rebinds all
21442 // keyboard events on a 'change' event
21443 mousetrap.bind('esc', this.escListener);
21444 };
21445
21446 /**
21447 * Deactivate the element
21448 * Overlay is displayed on top of the element
21449 */
21450 Activator.prototype.deactivate = function () {
21451 this.active = false;
21452 this.dom.overlay.style.display = '';
21453 util.removeClassName(this.dom.container, 'vis-active');
21454 mousetrap.unbind('esc', this.escListener);
21455
21456 this.emit('change');
21457 this.emit('deactivate');
21458 };
21459
21460 /**
21461 * Handle a tap event: activate the container
21462 * @param event
21463 * @private
21464 */
21465 Activator.prototype._onTapOverlay = function (event) {
21466 // activate the container
21467 this.activate();
21468 event.stopPropagation();
21469 };
21470
21471 /**
21472 * Test whether the element has the requested parent element somewhere in
21473 * its chain of parent nodes.
21474 * @param {HTMLElement} element
21475 * @param {HTMLElement} parent
21476 * @returns {boolean} Returns true when the parent is found somewhere in the
21477 * chain of parent nodes.
21478 * @private
21479 */
21480 function _hasParent(element, parent) {
21481 while (element) {
21482 if (element === parent) {
21483 return true
21484 }
21485 element = element.parentNode;
21486 }
21487 return false;
21488 }
21489
21490 module.exports = Activator;
21491
21492
21493/***/ },
21494/* 50 */
21495/***/ function(module, exports, __webpack_require__) {
21496
21497
21498 /**
21499 * Expose `Emitter`.
21500 */
21501
21502 module.exports = Emitter;
21503
21504 /**
21505 * Initialize a new `Emitter`.
21506 *
21507 * @api public
21508 */
21509
21510 function Emitter(obj) {
21511 if (obj) return mixin(obj);
21512 };
21513
21514 /**
21515 * Mixin the emitter properties.
21516 *
21517 * @param {Object} obj
21518 * @return {Object}
21519 * @api private
21520 */
21521
21522 function mixin(obj) {
21523 for (var key in Emitter.prototype) {
21524 obj[key] = Emitter.prototype[key];
21525 }
21526 return obj;
21527 }
21528
21529 /**
21530 * Listen on the given `event` with `fn`.
21531 *
21532 * @param {String} event
21533 * @param {Function} fn
21534 * @return {Emitter}
21535 * @api public
21536 */
21537
21538 Emitter.prototype.on =
21539 Emitter.prototype.addEventListener = function(event, fn){
21540 this._callbacks = this._callbacks || {};
21541 (this._callbacks[event] = this._callbacks[event] || [])
21542 .push(fn);
21543 return this;
21544 };
21545
21546 /**
21547 * Adds an `event` listener that will be invoked a single
21548 * time then automatically removed.
21549 *
21550 * @param {String} event
21551 * @param {Function} fn
21552 * @return {Emitter}
21553 * @api public
21554 */
21555
21556 Emitter.prototype.once = function(event, fn){
21557 var self = this;
21558 this._callbacks = this._callbacks || {};
21559
21560 function on() {
21561 self.off(event, on);
21562 fn.apply(this, arguments);
21563 }
21564
21565 on.fn = fn;
21566 this.on(event, on);
21567 return this;
21568 };
21569
21570 /**
21571 * Remove the given callback for `event` or all
21572 * registered callbacks.
21573 *
21574 * @param {String} event
21575 * @param {Function} fn
21576 * @return {Emitter}
21577 * @api public
21578 */
21579
21580 Emitter.prototype.off =
21581 Emitter.prototype.removeListener =
21582 Emitter.prototype.removeAllListeners =
21583 Emitter.prototype.removeEventListener = function(event, fn){
21584 this._callbacks = this._callbacks || {};
21585
21586 // all
21587 if (0 == arguments.length) {
21588 this._callbacks = {};
21589 return this;
21590 }
21591
21592 // specific event
21593 var callbacks = this._callbacks[event];
21594 if (!callbacks) return this;
21595
21596 // remove all handlers
21597 if (1 == arguments.length) {
21598 delete this._callbacks[event];
21599 return this;
21600 }
21601
21602 // remove specific handler
21603 var cb;
21604 for (var i = 0; i < callbacks.length; i++) {
21605 cb = callbacks[i];
21606 if (cb === fn || cb.fn === fn) {
21607 callbacks.splice(i, 1);
21608 break;
21609 }
21610 }
21611 return this;
21612 };
21613
21614 /**
21615 * Emit `event` with the given args.
21616 *
21617 * @param {String} event
21618 * @param {Mixed} ...
21619 * @return {Emitter}
21620 */
21621
21622 Emitter.prototype.emit = function(event){
21623 this._callbacks = this._callbacks || {};
21624 var args = [].slice.call(arguments, 1)
21625 , callbacks = this._callbacks[event];
21626
21627 if (callbacks) {
21628 callbacks = callbacks.slice(0);
21629 for (var i = 0, len = callbacks.length; i < len; ++i) {
21630 callbacks[i].apply(this, args);
21631 }
21632 }
21633
21634 return this;
21635 };
21636
21637 /**
21638 * Return array of callbacks for `event`.
21639 *
21640 * @param {String} event
21641 * @return {Array}
21642 * @api public
21643 */
21644
21645 Emitter.prototype.listeners = function(event){
21646 this._callbacks = this._callbacks || {};
21647 return this._callbacks[event] || [];
21648 };
21649
21650 /**
21651 * Check if this emitter has `event` handlers.
21652 *
21653 * @param {String} event
21654 * @return {Boolean}
21655 * @api public
21656 */
21657
21658 Emitter.prototype.hasListeners = function(event){
21659 return !! this.listeners(event).length;
21660 };
21661
21662
21663/***/ },
21664/* 51 */
21665/***/ function(module, exports, __webpack_require__) {
21666
21667 /**
21668 * Copyright 2012 Craig Campbell
21669 *
21670 * Licensed under the Apache License, Version 2.0 (the "License");
21671 * you may not use this file except in compliance with the License.
21672 * You may obtain a copy of the License at
21673 *
21674 * http://www.apache.org/licenses/LICENSE-2.0
21675 *
21676 * Unless required by applicable law or agreed to in writing, software
21677 * distributed under the License is distributed on an "AS IS" BASIS,
21678 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21679 * See the License for the specific language governing permissions and
21680 * limitations under the License.
21681 *
21682 * Mousetrap is a simple keyboard shortcut library for Javascript with
21683 * no external dependencies
21684 *
21685 * @version 1.1.2
21686 * @url craig.is/killing/mice
21687 */
21688
21689 /**
21690 * mapping of special keycodes to their corresponding keys
21691 *
21692 * everything in this dictionary cannot use keypress events
21693 * so it has to be here to map to the correct keycodes for
21694 * keyup/keydown events
21695 *
21696 * @type {Object}
21697 */
21698 var _MAP = {
21699 8: 'backspace',
21700 9: 'tab',
21701 13: 'enter',
21702 16: 'shift',
21703 17: 'ctrl',
21704 18: 'alt',
21705 20: 'capslock',
21706 27: 'esc',
21707 32: 'space',
21708 33: 'pageup',
21709 34: 'pagedown',
21710 35: 'end',
21711 36: 'home',
21712 37: 'left',
21713 38: 'up',
21714 39: 'right',
21715 40: 'down',
21716 45: 'ins',
21717 46: 'del',
21718 91: 'meta',
21719 93: 'meta',
21720 224: 'meta'
21721 },
21722
21723 /**
21724 * mapping for special characters so they can support
21725 *
21726 * this dictionary is only used incase you want to bind a
21727 * keyup or keydown event to one of these keys
21728 *
21729 * @type {Object}
21730 */
21731 _KEYCODE_MAP = {
21732 106: '*',
21733 107: '+',
21734 109: '-',
21735 110: '.',
21736 111 : '/',
21737 186: ';',
21738 187: '=',
21739 188: ',',
21740 189: '-',
21741 190: '.',
21742 191: '/',
21743 192: '`',
21744 219: '[',
21745 220: '\\',
21746 221: ']',
21747 222: '\''
21748 },
21749
21750 /**
21751 * this is a mapping of keys that require shift on a US keypad
21752 * back to the non shift equivelents
21753 *
21754 * this is so you can use keyup events with these keys
21755 *
21756 * note that this will only work reliably on US keyboards
21757 *
21758 * @type {Object}
21759 */
21760 _SHIFT_MAP = {
21761 '~': '`',
21762 '!': '1',
21763 '@': '2',
21764 '#': '3',
21765 '$': '4',
21766 '%': '5',
21767 '^': '6',
21768 '&': '7',
21769 '*': '8',
21770 '(': '9',
21771 ')': '0',
21772 '_': '-',
21773 '+': '=',
21774 ':': ';',
21775 '\"': '\'',
21776 '<': ',',
21777 '>': '.',
21778 '?': '/',
21779 '|': '\\'
21780 },
21781
21782 /**
21783 * this is a list of special strings you can use to map
21784 * to modifier keys when you specify your keyboard shortcuts
21785 *
21786 * @type {Object}
21787 */
21788 _SPECIAL_ALIASES = {
21789 'option': 'alt',
21790 'command': 'meta',
21791 'return': 'enter',
21792 'escape': 'esc'
21793 },
21794
21795 /**
21796 * variable to store the flipped version of _MAP from above
21797 * needed to check if we should use keypress or not when no action
21798 * is specified
21799 *
21800 * @type {Object|undefined}
21801 */
21802 _REVERSE_MAP,
21803
21804 /**
21805 * a list of all the callbacks setup via Mousetrap.bind()
21806 *
21807 * @type {Object}
21808 */
21809 _callbacks = {},
21810
21811 /**
21812 * direct map of string combinations to callbacks used for trigger()
21813 *
21814 * @type {Object}
21815 */
21816 _direct_map = {},
21817
21818 /**
21819 * keeps track of what level each sequence is at since multiple
21820 * sequences can start out with the same sequence
21821 *
21822 * @type {Object}
21823 */
21824 _sequence_levels = {},
21825
21826 /**
21827 * variable to store the setTimeout call
21828 *
21829 * @type {null|number}
21830 */
21831 _reset_timer,
21832
21833 /**
21834 * temporary state where we will ignore the next keyup
21835 *
21836 * @type {boolean|string}
21837 */
21838 _ignore_next_keyup = false,
21839
21840 /**
21841 * are we currently inside of a sequence?
21842 * type of action ("keyup" or "keydown" or "keypress") or false
21843 *
21844 * @type {boolean|string}
21845 */
21846 _inside_sequence = false;
21847
21848 /**
21849 * loop through the f keys, f1 to f19 and add them to the map
21850 * programatically
21851 */
21852 for (var i = 1; i < 20; ++i) {
21853 _MAP[111 + i] = 'f' + i;
21854 }
21855
21856 /**
21857 * loop through to map numbers on the numeric keypad
21858 */
21859 for (i = 0; i <= 9; ++i) {
21860 _MAP[i + 96] = i;
21861 }
21862
21863 /**
21864 * cross browser add event method
21865 *
21866 * @param {Element|HTMLDocument} object
21867 * @param {string} type
21868 * @param {Function} callback
21869 * @returns void
21870 */
21871 function _addEvent(object, type, callback) {
21872 if (object.addEventListener) {
21873 return object.addEventListener(type, callback, false);
21874 }
21875
21876 object.attachEvent('on' + type, callback);
21877 }
21878
21879 /**
21880 * takes the event and returns the key character
21881 *
21882 * @param {Event} e
21883 * @return {string}
21884 */
21885 function _characterFromEvent(e) {
21886
21887 // for keypress events we should return the character as is
21888 if (e.type == 'keypress') {
21889 return String.fromCharCode(e.which);
21890 }
21891
21892 // for non keypress events the special maps are needed
21893 if (_MAP[e.which]) {
21894 return _MAP[e.which];
21895 }
21896
21897 if (_KEYCODE_MAP[e.which]) {
21898 return _KEYCODE_MAP[e.which];
21899 }
21900
21901 // if it is not in the special map
21902 return String.fromCharCode(e.which).toLowerCase();
21903 }
21904
21905 /**
21906 * should we stop this event before firing off callbacks
21907 *
21908 * @param {Event} e
21909 * @return {boolean}
21910 */
21911 function _stop(e) {
21912 var element = e.target || e.srcElement,
21913 tag_name = element.tagName;
21914
21915 // if the element has the class "mousetrap" then no need to stop
21916 if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
21917 return false;
21918 }
21919
21920 // stop for input, select, and textarea
21921 return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
21922 }
21923
21924 /**
21925 * checks if two arrays are equal
21926 *
21927 * @param {Array} modifiers1
21928 * @param {Array} modifiers2
21929 * @returns {boolean}
21930 */
21931 function _modifiersMatch(modifiers1, modifiers2) {
21932 return modifiers1.sort().join(',') === modifiers2.sort().join(',');
21933 }
21934
21935 /**
21936 * resets all sequence counters except for the ones passed in
21937 *
21938 * @param {Object} do_not_reset
21939 * @returns void
21940 */
21941 function _resetSequences(do_not_reset) {
21942 do_not_reset = do_not_reset || {};
21943
21944 var active_sequences = false,
21945 key;
21946
21947 for (key in _sequence_levels) {
21948 if (do_not_reset[key]) {
21949 active_sequences = true;
21950 continue;
21951 }
21952 _sequence_levels[key] = 0;
21953 }
21954
21955 if (!active_sequences) {
21956 _inside_sequence = false;
21957 }
21958 }
21959
21960 /**
21961 * finds all callbacks that match based on the keycode, modifiers,
21962 * and action
21963 *
21964 * @param {string} character
21965 * @param {Array} modifiers
21966 * @param {string} action
21967 * @param {boolean=} remove - should we remove any matches
21968 * @param {string=} combination
21969 * @returns {Array}
21970 */
21971 function _getMatches(character, modifiers, action, remove, combination) {
21972 var i,
21973 callback,
21974 matches = [];
21975
21976 // if there are no events related to this keycode
21977 if (!_callbacks[character]) {
21978 return [];
21979 }
21980
21981 // if a modifier key is coming up on its own we should allow it
21982 if (action == 'keyup' && _isModifier(character)) {
21983 modifiers = [character];
21984 }
21985
21986 // loop through all callbacks for the key that was pressed
21987 // and see if any of them match
21988 for (i = 0; i < _callbacks[character].length; ++i) {
21989 callback = _callbacks[character][i];
21990
21991 // if this is a sequence but it is not at the right level
21992 // then move onto the next match
21993 if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
21994 continue;
21995 }
21996
21997 // if the action we are looking for doesn't match the action we got
21998 // then we should keep going
21999 if (action != callback.action) {
22000 continue;
22001 }
22002
22003 // if this is a keypress event that means that we need to only
22004 // look at the character, otherwise check the modifiers as
22005 // well
22006 if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
22007
22008 // remove is used so if you change your mind and call bind a
22009 // second time with a new function the first one is overwritten
22010 if (remove && callback.combo == combination) {
22011 _callbacks[character].splice(i, 1);
22012 }
22013
22014 matches.push(callback);
22015 }
22016 }
22017
22018 return matches;
22019 }
22020
22021 /**
22022 * takes a key event and figures out what the modifiers are
22023 *
22024 * @param {Event} e
22025 * @returns {Array}
22026 */
22027 function _eventModifiers(e) {
22028 var modifiers = [];
22029
22030 if (e.shiftKey) {
22031 modifiers.push('shift');
22032 }
22033
22034 if (e.altKey) {
22035 modifiers.push('alt');
22036 }
22037
22038 if (e.ctrlKey) {
22039 modifiers.push('ctrl');
22040 }
22041
22042 if (e.metaKey) {
22043 modifiers.push('meta');
22044 }
22045
22046 return modifiers;
22047 }
22048
22049 /**
22050 * actually calls the callback function
22051 *
22052 * if your callback function returns false this will use the jquery
22053 * convention - prevent default and stop propogation on the event
22054 *
22055 * @param {Function} callback
22056 * @param {Event} e
22057 * @returns void
22058 */
22059 function _fireCallback(callback, e) {
22060 if (callback(e) === false) {
22061 if (e.preventDefault) {
22062 e.preventDefault();
22063 }
22064
22065 if (e.stopPropagation) {
22066 e.stopPropagation();
22067 }
22068
22069 e.returnValue = false;
22070 e.cancelBubble = true;
22071 }
22072 }
22073
22074 /**
22075 * handles a character key event
22076 *
22077 * @param {string} character
22078 * @param {Event} e
22079 * @returns void
22080 */
22081 function _handleCharacter(character, e) {
22082
22083 // if this event should not happen stop here
22084 if (_stop(e)) {
22085 return;
22086 }
22087
22088 var callbacks = _getMatches(character, _eventModifiers(e), e.type),
22089 i,
22090 do_not_reset = {},
22091 processed_sequence_callback = false;
22092
22093 // loop through matching callbacks for this key event
22094 for (i = 0; i < callbacks.length; ++i) {
22095
22096 // fire for all sequence callbacks
22097 // this is because if for example you have multiple sequences
22098 // bound such as "g i" and "g t" they both need to fire the
22099 // callback for matching g cause otherwise you can only ever
22100 // match the first one
22101 if (callbacks[i].seq) {
22102 processed_sequence_callback = true;
22103
22104 // keep a list of which sequences were matches for later
22105 do_not_reset[callbacks[i].seq] = 1;
22106 _fireCallback(callbacks[i].callback, e);
22107 continue;
22108 }
22109
22110 // if there were no sequence matches but we are still here
22111 // that means this is a regular match so we should fire that
22112 if (!processed_sequence_callback && !_inside_sequence) {
22113 _fireCallback(callbacks[i].callback, e);
22114 }
22115 }
22116
22117 // if you are inside of a sequence and the key you are pressing
22118 // is not a modifier key then we should reset all sequences
22119 // that were not matched by this key event
22120 if (e.type == _inside_sequence && !_isModifier(character)) {
22121 _resetSequences(do_not_reset);
22122 }
22123 }
22124
22125 /**
22126 * handles a keydown event
22127 *
22128 * @param {Event} e
22129 * @returns void
22130 */
22131 function _handleKey(e) {
22132
22133 // normalize e.which for key events
22134 // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
22135 e.which = typeof e.which == "number" ? e.which : e.keyCode;
22136
22137 var character = _characterFromEvent(e);
22138
22139 // no character found then stop
22140 if (!character) {
22141 return;
22142 }
22143
22144 if (e.type == 'keyup' && _ignore_next_keyup == character) {
22145 _ignore_next_keyup = false;
22146 return;
22147 }
22148
22149 _handleCharacter(character, e);
22150 }
22151
22152 /**
22153 * determines if the keycode specified is a modifier key or not
22154 *
22155 * @param {string} key
22156 * @returns {boolean}
22157 */
22158 function _isModifier(key) {
22159 return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
22160 }
22161
22162 /**
22163 * called to set a 1 second timeout on the specified sequence
22164 *
22165 * this is so after each key press in the sequence you have 1 second
22166 * to press the next key before you have to start over
22167 *
22168 * @returns void
22169 */
22170 function _resetSequenceTimer() {
22171 clearTimeout(_reset_timer);
22172 _reset_timer = setTimeout(_resetSequences, 1000);
22173 }
22174
22175 /**
22176 * reverses the map lookup so that we can look for specific keys
22177 * to see what can and can't use keypress
22178 *
22179 * @return {Object}
22180 */
22181 function _getReverseMap() {
22182 if (!_REVERSE_MAP) {
22183 _REVERSE_MAP = {};
22184 for (var key in _MAP) {
22185
22186 // pull out the numeric keypad from here cause keypress should
22187 // be able to detect the keys from the character
22188 if (key > 95 && key < 112) {
22189 continue;
22190 }
22191
22192 if (_MAP.hasOwnProperty(key)) {
22193 _REVERSE_MAP[_MAP[key]] = key;
22194 }
22195 }
22196 }
22197 return _REVERSE_MAP;
22198 }
22199
22200 /**
22201 * picks the best action based on the key combination
22202 *
22203 * @param {string} key - character for key
22204 * @param {Array} modifiers
22205 * @param {string=} action passed in
22206 */
22207 function _pickBestAction(key, modifiers, action) {
22208
22209 // if no action was picked in we should try to pick the one
22210 // that we think would work best for this key
22211 if (!action) {
22212 action = _getReverseMap()[key] ? 'keydown' : 'keypress';
22213 }
22214
22215 // modifier keys don't work as expected with keypress,
22216 // switch to keydown
22217 if (action == 'keypress' && modifiers.length) {
22218 action = 'keydown';
22219 }
22220
22221 return action;
22222 }
22223
22224 /**
22225 * binds a key sequence to an event
22226 *
22227 * @param {string} combo - combo specified in bind call
22228 * @param {Array} keys
22229 * @param {Function} callback
22230 * @param {string=} action
22231 * @returns void
22232 */
22233 function _bindSequence(combo, keys, callback, action) {
22234
22235 // start off by adding a sequence level record for this combination
22236 // and setting the level to 0
22237 _sequence_levels[combo] = 0;
22238
22239 // if there is no action pick the best one for the first key
22240 // in the sequence
22241 if (!action) {
22242 action = _pickBestAction(keys[0], []);
22243 }
22244
22245 /**
22246 * callback to increase the sequence level for this sequence and reset
22247 * all other sequences that were active
22248 *
22249 * @param {Event} e
22250 * @returns void
22251 */
22252 var _increaseSequence = function(e) {
22253 _inside_sequence = action;
22254 ++_sequence_levels[combo];
22255 _resetSequenceTimer();
22256 },
22257
22258 /**
22259 * wraps the specified callback inside of another function in order
22260 * to reset all sequence counters as soon as this sequence is done
22261 *
22262 * @param {Event} e
22263 * @returns void
22264 */
22265 _callbackAndReset = function(e) {
22266 _fireCallback(callback, e);
22267
22268 // we should ignore the next key up if the action is key down
22269 // or keypress. this is so if you finish a sequence and
22270 // release the key the final key will not trigger a keyup
22271 if (action !== 'keyup') {
22272 _ignore_next_keyup = _characterFromEvent(e);
22273 }
22274
22275 // weird race condition if a sequence ends with the key
22276 // another sequence begins with
22277 setTimeout(_resetSequences, 10);
22278 },
22279 i;
22280
22281 // loop through keys one at a time and bind the appropriate callback
22282 // function. for any key leading up to the final one it should
22283 // increase the sequence. after the final, it should reset all sequences
22284 for (i = 0; i < keys.length; ++i) {
22285 _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
22286 }
22287 }
22288
22289 /**
22290 * binds a single keyboard combination
22291 *
22292 * @param {string} combination
22293 * @param {Function} callback
22294 * @param {string=} action
22295 * @param {string=} sequence_name - name of sequence if part of sequence
22296 * @param {number=} level - what part of the sequence the command is
22297 * @returns void
22298 */
22299 function _bindSingle(combination, callback, action, sequence_name, level) {
22300
22301 // make sure multiple spaces in a row become a single space
22302 combination = combination.replace(/\s+/g, ' ');
22303
22304 var sequence = combination.split(' '),
22305 i,
22306 key,
22307 keys,
22308 modifiers = [];
22309
22310 // if this pattern is a sequence of keys then run through this method
22311 // to reprocess each pattern one key at a time
22312 if (sequence.length > 1) {
22313 return _bindSequence(combination, sequence, callback, action);
22314 }
22315
22316 // take the keys from this pattern and figure out what the actual
22317 // pattern is all about
22318 keys = combination === '+' ? ['+'] : combination.split('+');
22319
22320 for (i = 0; i < keys.length; ++i) {
22321 key = keys[i];
22322
22323 // normalize key names
22324 if (_SPECIAL_ALIASES[key]) {
22325 key = _SPECIAL_ALIASES[key];
22326 }
22327
22328 // if this is not a keypress event then we should
22329 // be smart about using shift keys
22330 // this will only work for US keyboards however
22331 if (action && action != 'keypress' && _SHIFT_MAP[key]) {
22332 key = _SHIFT_MAP[key];
22333 modifiers.push('shift');
22334 }
22335
22336 // if this key is a modifier then add it to the list of modifiers
22337 if (_isModifier(key)) {
22338 modifiers.push(key);
22339 }
22340 }
22341
22342 // depending on what the key combination is
22343 // we will try to pick the best event for it
22344 action = _pickBestAction(key, modifiers, action);
22345
22346 // make sure to initialize array if this is the first time
22347 // a callback is added for this key
22348 if (!_callbacks[key]) {
22349 _callbacks[key] = [];
22350 }
22351
22352 // remove an existing match if there is one
22353 _getMatches(key, modifiers, action, !sequence_name, combination);
22354
22355 // add this call back to the array
22356 // if it is a sequence put it at the beginning
22357 // if not put it at the end
22358 //
22359 // this is important because the way these are processed expects
22360 // the sequence ones to come first
22361 _callbacks[key][sequence_name ? 'unshift' : 'push']({
22362 callback: callback,
22363 modifiers: modifiers,
22364 action: action,
22365 seq: sequence_name,
22366 level: level,
22367 combo: combination
22368 });
22369 }
22370
22371 /**
22372 * binds multiple combinations to the same callback
22373 *
22374 * @param {Array} combinations
22375 * @param {Function} callback
22376 * @param {string|undefined} action
22377 * @returns void
22378 */
22379 function _bindMultiple(combinations, callback, action) {
22380 for (var i = 0; i < combinations.length; ++i) {
22381 _bindSingle(combinations[i], callback, action);
22382 }
22383 }
22384
22385 // start!
22386 _addEvent(document, 'keypress', _handleKey);
22387 _addEvent(document, 'keydown', _handleKey);
22388 _addEvent(document, 'keyup', _handleKey);
22389
22390 var mousetrap = {
22391
22392 /**
22393 * binds an event to mousetrap
22394 *
22395 * can be a single key, a combination of keys separated with +,
22396 * a comma separated list of keys, an array of keys, or
22397 * a sequence of keys separated by spaces
22398 *
22399 * be sure to list the modifier keys first to make sure that the
22400 * correct key ends up getting bound (the last key in the pattern)
22401 *
22402 * @param {string|Array} keys
22403 * @param {Function} callback
22404 * @param {string=} action - 'keypress', 'keydown', or 'keyup'
22405 * @returns void
22406 */
22407 bind: function(keys, callback, action) {
22408 _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
22409 _direct_map[keys + ':' + action] = callback;
22410 return this;
22411 },
22412
22413 /**
22414 * unbinds an event to mousetrap
22415 *
22416 * the unbinding sets the callback function of the specified key combo
22417 * to an empty function and deletes the corresponding key in the
22418 * _direct_map dict.
22419 *
22420 * the keycombo+action has to be exactly the same as
22421 * it was defined in the bind method
22422 *
22423 * TODO: actually remove this from the _callbacks dictionary instead
22424 * of binding an empty function
22425 *
22426 * @param {string|Array} keys
22427 * @param {string} action
22428 * @returns void
22429 */
22430 unbind: function(keys, action) {
22431 if (_direct_map[keys + ':' + action]) {
22432 delete _direct_map[keys + ':' + action];
22433 this.bind(keys, function() {}, action);
22434 }
22435 return this;
22436 },
22437
22438 /**
22439 * triggers an event that has already been bound
22440 *
22441 * @param {string} keys
22442 * @param {string=} action
22443 * @returns void
22444 */
22445 trigger: function(keys, action) {
22446 _direct_map[keys + ':' + action]();
22447 return this;
22448 },
22449
22450 /**
22451 * resets the library back to its initial state. this is useful
22452 * if you want to clear out the current keyboard shortcuts and bind
22453 * new ones - for example if you switch to another page
22454 *
22455 * @returns void
22456 */
22457 reset: function() {
22458 _callbacks = {};
22459 _direct_map = {};
22460 return this;
22461 }
22462 };
22463
22464 module.exports = mousetrap;
22465
22466
22467
22468/***/ },
22469/* 52 */
22470/***/ function(module, exports, __webpack_require__) {
22471
22472 var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js
22473 //! version : 2.8.3
22474 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
22475 //! license : MIT
22476 //! momentjs.com
22477
22478 (function (undefined) {
22479 /************************************
22480 Constants
22481 ************************************/
22482
22483 var moment,
22484 VERSION = '2.8.3',
22485 // the global-scope this is NOT the global object in Node.js
22486 globalScope = typeof global !== 'undefined' ? global : this,
22487 oldGlobalMoment,
22488 round = Math.round,
22489 hasOwnProperty = Object.prototype.hasOwnProperty,
22490 i,
22491
22492 YEAR = 0,
22493 MONTH = 1,
22494 DATE = 2,
22495 HOUR = 3,
22496 MINUTE = 4,
22497 SECOND = 5,
22498 MILLISECOND = 6,
22499
22500 // internal storage for locale config files
22501 locales = {},
22502
22503 // extra moment internal properties (plugins register props here)
22504 momentProperties = [],
22505
22506 // check for nodeJS
22507 hasModule = (typeof module !== 'undefined' && module.exports),
22508
22509 // ASP.NET json date format regex
22510 aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
22511 aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
22512
22513 // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
22514 // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
22515 isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
22516
22517 // format tokens
22518 formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
22519 localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
22520
22521 // parsing token regexes
22522 parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
22523 parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
22524 parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
22525 parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
22526 parseTokenDigits = /\d+/, // nonzero number of digits
22527 parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
22528 parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
22529 parseTokenT = /T/i, // T (ISO separator)
22530 parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
22531 parseTokenOrdinal = /\d{1,2}/,
22532
22533 //strict parsing regexes
22534 parseTokenOneDigit = /\d/, // 0 - 9
22535 parseTokenTwoDigits = /\d\d/, // 00 - 99
22536 parseTokenThreeDigits = /\d{3}/, // 000 - 999
22537 parseTokenFourDigits = /\d{4}/, // 0000 - 9999
22538 parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
22539 parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
22540
22541 // iso 8601 regex
22542 // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
22543 isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
22544
22545 isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
22546
22547 isoDates = [
22548 ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
22549 ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
22550 ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
22551 ['GGGG-[W]WW', /\d{4}-W\d{2}/],
22552 ['YYYY-DDD', /\d{4}-\d{3}/]
22553 ],
22554
22555 // iso time formats and regexes
22556 isoTimes = [
22557 ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
22558 ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
22559 ['HH:mm', /(T| )\d\d:\d\d/],
22560 ['HH', /(T| )\d\d/]
22561 ],
22562
22563 // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-15', '30']
22564 parseTimezoneChunker = /([\+\-]|\d\d)/gi,
22565
22566 // getter and setter names
22567 proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
22568 unitMillisecondFactors = {
22569 'Milliseconds' : 1,
22570 'Seconds' : 1e3,
22571 'Minutes' : 6e4,
22572 'Hours' : 36e5,
22573 'Days' : 864e5,
22574 'Months' : 2592e6,
22575 'Years' : 31536e6
22576 },
22577
22578 unitAliases = {
22579 ms : 'millisecond',
22580 s : 'second',
22581 m : 'minute',
22582 h : 'hour',
22583 d : 'day',
22584 D : 'date',
22585 w : 'week',
22586 W : 'isoWeek',
22587 M : 'month',
22588 Q : 'quarter',
22589 y : 'year',
22590 DDD : 'dayOfYear',
22591 e : 'weekday',
22592 E : 'isoWeekday',
22593 gg: 'weekYear',
22594 GG: 'isoWeekYear'
22595 },
22596
22597 camelFunctions = {
22598 dayofyear : 'dayOfYear',
22599 isoweekday : 'isoWeekday',
22600 isoweek : 'isoWeek',
22601 weekyear : 'weekYear',
22602 isoweekyear : 'isoWeekYear'
22603 },
22604
22605 // format function strings
22606 formatFunctions = {},
22607
22608 // default relative time thresholds
22609 relativeTimeThresholds = {
22610 s: 45, // seconds to minute
22611 m: 45, // minutes to hour
22612 h: 22, // hours to day
22613 d: 26, // days to month
22614 M: 11 // months to year
22615 },
22616
22617 // tokens to ordinalize and pad
22618 ordinalizeTokens = 'DDD w W M D d'.split(' '),
22619 paddedTokens = 'M D H h m s w W'.split(' '),
22620
22621 formatTokenFunctions = {
22622 M : function () {
22623 return this.month() + 1;
22624 },
22625 MMM : function (format) {
22626 return this.localeData().monthsShort(this, format);
22627 },
22628 MMMM : function (format) {
22629 return this.localeData().months(this, format);
22630 },
22631 D : function () {
22632 return this.date();
22633 },
22634 DDD : function () {
22635 return this.dayOfYear();
22636 },
22637 d : function () {
22638 return this.day();
22639 },
22640 dd : function (format) {
22641 return this.localeData().weekdaysMin(this, format);
22642 },
22643 ddd : function (format) {
22644 return this.localeData().weekdaysShort(this, format);
22645 },
22646 dddd : function (format) {
22647 return this.localeData().weekdays(this, format);
22648 },
22649 w : function () {
22650 return this.week();
22651 },
22652 W : function () {
22653 return this.isoWeek();
22654 },
22655 YY : function () {
22656 return leftZeroFill(this.year() % 100, 2);
22657 },
22658 YYYY : function () {
22659 return leftZeroFill(this.year(), 4);
22660 },
22661 YYYYY : function () {
22662 return leftZeroFill(this.year(), 5);
22663 },
22664 YYYYYY : function () {
22665 var y = this.year(), sign = y >= 0 ? '+' : '-';
22666 return sign + leftZeroFill(Math.abs(y), 6);
22667 },
22668 gg : function () {
22669 return leftZeroFill(this.weekYear() % 100, 2);
22670 },
22671 gggg : function () {
22672 return leftZeroFill(this.weekYear(), 4);
22673 },
22674 ggggg : function () {
22675 return leftZeroFill(this.weekYear(), 5);
22676 },
22677 GG : function () {
22678 return leftZeroFill(this.isoWeekYear() % 100, 2);
22679 },
22680 GGGG : function () {
22681 return leftZeroFill(this.isoWeekYear(), 4);
22682 },
22683 GGGGG : function () {
22684 return leftZeroFill(this.isoWeekYear(), 5);
22685 },
22686 e : function () {
22687 return this.weekday();
22688 },
22689 E : function () {
22690 return this.isoWeekday();
22691 },
22692 a : function () {
22693 return this.localeData().meridiem(this.hours(), this.minutes(), true);
22694 },
22695 A : function () {
22696 return this.localeData().meridiem(this.hours(), this.minutes(), false);
22697 },
22698 H : function () {
22699 return this.hours();
22700 },
22701 h : function () {
22702 return this.hours() % 12 || 12;
22703 },
22704 m : function () {
22705 return this.minutes();
22706 },
22707 s : function () {
22708 return this.seconds();
22709 },
22710 S : function () {
22711 return toInt(this.milliseconds() / 100);
22712 },
22713 SS : function () {
22714 return leftZeroFill(toInt(this.milliseconds() / 10), 2);
22715 },
22716 SSS : function () {
22717 return leftZeroFill(this.milliseconds(), 3);
22718 },
22719 SSSS : function () {
22720 return leftZeroFill(this.milliseconds(), 3);
22721 },
22722 Z : function () {
22723 var a = -this.zone(),
22724 b = '+';
22725 if (a < 0) {
22726 a = -a;
22727 b = '-';
22728 }
22729 return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2);
22730 },
22731 ZZ : function () {
22732 var a = -this.zone(),
22733 b = '+';
22734 if (a < 0) {
22735 a = -a;
22736 b = '-';
22737 }
22738 return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
22739 },
22740 z : function () {
22741 return this.zoneAbbr();
22742 },
22743 zz : function () {
22744 return this.zoneName();
22745 },
22746 X : function () {
22747 return this.unix();
22748 },
22749 Q : function () {
22750 return this.quarter();
22751 }
22752 },
22753
22754 deprecations = {},
22755
22756 lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
22757
22758 // Pick the first defined of two or three arguments. dfl comes from
22759 // default.
22760 function dfl(a, b, c) {
22761 switch (arguments.length) {
22762 case 2: return a != null ? a : b;
22763 case 3: return a != null ? a : b != null ? b : c;
22764 default: throw new Error('Implement me');
22765 }
22766 }
22767
22768 function hasOwnProp(a, b) {
22769 return hasOwnProperty.call(a, b);
22770 }
22771
22772 function defaultParsingFlags() {
22773 // We need to deep clone this object, and es5 standard is not very
22774 // helpful.
22775 return {
22776 empty : false,
22777 unusedTokens : [],
22778 unusedInput : [],
22779 overflow : -2,
22780 charsLeftOver : 0,
22781 nullInput : false,
22782 invalidMonth : null,
22783 invalidFormat : false,
22784 userInvalidated : false,
22785 iso: false
22786 };
22787 }
22788
22789 function printMsg(msg) {
22790 if (moment.suppressDeprecationWarnings === false &&
22791 typeof console !== 'undefined' && console.warn) {
22792 console.warn('Deprecation warning: ' + msg);
22793 }
22794 }
22795
22796 function deprecate(msg, fn) {
22797 var firstTime = true;
22798 return extend(function () {
22799 if (firstTime) {
22800 printMsg(msg);
22801 firstTime = false;
22802 }
22803 return fn.apply(this, arguments);
22804 }, fn);
22805 }
22806
22807 function deprecateSimple(name, msg) {
22808 if (!deprecations[name]) {
22809 printMsg(msg);
22810 deprecations[name] = true;
22811 }
22812 }
22813
22814 function padToken(func, count) {
22815 return function (a) {
22816 return leftZeroFill(func.call(this, a), count);
22817 };
22818 }
22819 function ordinalizeToken(func, period) {
22820 return function (a) {
22821 return this.localeData().ordinal(func.call(this, a), period);
22822 };
22823 }
22824
22825 while (ordinalizeTokens.length) {
22826 i = ordinalizeTokens.pop();
22827 formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
22828 }
22829 while (paddedTokens.length) {
22830 i = paddedTokens.pop();
22831 formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
22832 }
22833 formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
22834
22835
22836 /************************************
22837 Constructors
22838 ************************************/
22839
22840 function Locale() {
22841 }
22842
22843 // Moment prototype object
22844 function Moment(config, skipOverflow) {
22845 if (skipOverflow !== false) {
22846 checkOverflow(config);
22847 }
22848 copyConfig(this, config);
22849 this._d = new Date(+config._d);
22850 }
22851
22852 // Duration Constructor
22853 function Duration(duration) {
22854 var normalizedInput = normalizeObjectUnits(duration),
22855 years = normalizedInput.year || 0,
22856 quarters = normalizedInput.quarter || 0,
22857 months = normalizedInput.month || 0,
22858 weeks = normalizedInput.week || 0,
22859 days = normalizedInput.day || 0,
22860 hours = normalizedInput.hour || 0,
22861 minutes = normalizedInput.minute || 0,
22862 seconds = normalizedInput.second || 0,
22863 milliseconds = normalizedInput.millisecond || 0;
22864
22865 // representation for dateAddRemove
22866 this._milliseconds = +milliseconds +
22867 seconds * 1e3 + // 1000
22868 minutes * 6e4 + // 1000 * 60
22869 hours * 36e5; // 1000 * 60 * 60
22870 // Because of dateAddRemove treats 24 hours as different from a
22871 // day when working around DST, we need to store them separately
22872 this._days = +days +
22873 weeks * 7;
22874 // It is impossible translate months into days without knowing
22875 // which months you are are talking about, so we have to store
22876 // it separately.
22877 this._months = +months +
22878 quarters * 3 +
22879 years * 12;
22880
22881 this._data = {};
22882
22883 this._locale = moment.localeData();
22884
22885 this._bubble();
22886 }
22887
22888 /************************************
22889 Helpers
22890 ************************************/
22891
22892
22893 function extend(a, b) {
22894 for (var i in b) {
22895 if (hasOwnProp(b, i)) {
22896 a[i] = b[i];
22897 }
22898 }
22899
22900 if (hasOwnProp(b, 'toString')) {
22901 a.toString = b.toString;
22902 }
22903
22904 if (hasOwnProp(b, 'valueOf')) {
22905 a.valueOf = b.valueOf;
22906 }
22907
22908 return a;
22909 }
22910
22911 function copyConfig(to, from) {
22912 var i, prop, val;
22913
22914 if (typeof from._isAMomentObject !== 'undefined') {
22915 to._isAMomentObject = from._isAMomentObject;
22916 }
22917 if (typeof from._i !== 'undefined') {
22918 to._i = from._i;
22919 }
22920 if (typeof from._f !== 'undefined') {
22921 to._f = from._f;
22922 }
22923 if (typeof from._l !== 'undefined') {
22924 to._l = from._l;
22925 }
22926 if (typeof from._strict !== 'undefined') {
22927 to._strict = from._strict;
22928 }
22929 if (typeof from._tzm !== 'undefined') {
22930 to._tzm = from._tzm;
22931 }
22932 if (typeof from._isUTC !== 'undefined') {
22933 to._isUTC = from._isUTC;
22934 }
22935 if (typeof from._offset !== 'undefined') {
22936 to._offset = from._offset;
22937 }
22938 if (typeof from._pf !== 'undefined') {
22939 to._pf = from._pf;
22940 }
22941 if (typeof from._locale !== 'undefined') {
22942 to._locale = from._locale;
22943 }
22944
22945 if (momentProperties.length > 0) {
22946 for (i in momentProperties) {
22947 prop = momentProperties[i];
22948 val = from[prop];
22949 if (typeof val !== 'undefined') {
22950 to[prop] = val;
22951 }
22952 }
22953 }
22954
22955 return to;
22956 }
22957
22958 function absRound(number) {
22959 if (number < 0) {
22960 return Math.ceil(number);
22961 } else {
22962 return Math.floor(number);
22963 }
22964 }
22965
22966 // left zero fill a number
22967 // see http://jsperf.com/left-zero-filling for performance comparison
22968 function leftZeroFill(number, targetLength, forceSign) {
22969 var output = '' + Math.abs(number),
22970 sign = number >= 0;
22971
22972 while (output.length < targetLength) {
22973 output = '0' + output;
22974 }
22975 return (sign ? (forceSign ? '+' : '') : '-') + output;
22976 }
22977
22978 function positiveMomentsDifference(base, other) {
22979 var res = {milliseconds: 0, months: 0};
22980
22981 res.months = other.month() - base.month() +
22982 (other.year() - base.year()) * 12;
22983 if (base.clone().add(res.months, 'M').isAfter(other)) {
22984 --res.months;
22985 }
22986
22987 res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
22988
22989 return res;
22990 }
22991
22992 function momentsDifference(base, other) {
22993 var res;
22994 other = makeAs(other, base);
22995 if (base.isBefore(other)) {
22996 res = positiveMomentsDifference(base, other);
22997 } else {
22998 res = positiveMomentsDifference(other, base);
22999 res.milliseconds = -res.milliseconds;
23000 res.months = -res.months;
23001 }
23002
23003 return res;
23004 }
23005
23006 // TODO: remove 'name' arg after deprecation is removed
23007 function createAdder(direction, name) {
23008 return function (val, period) {
23009 var dur, tmp;
23010 //invert the arguments, but complain about it
23011 if (period !== null && !isNaN(+period)) {
23012 deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).');
23013 tmp = val; val = period; period = tmp;
23014 }
23015
23016 val = typeof val === 'string' ? +val : val;
23017 dur = moment.duration(val, period);
23018 addOrSubtractDurationFromMoment(this, dur, direction);
23019 return this;
23020 };
23021 }
23022
23023 function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
23024 var milliseconds = duration._milliseconds,
23025 days = duration._days,
23026 months = duration._months;
23027 updateOffset = updateOffset == null ? true : updateOffset;
23028
23029 if (milliseconds) {
23030 mom._d.setTime(+mom._d + milliseconds * isAdding);
23031 }
23032 if (days) {
23033 rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
23034 }
23035 if (months) {
23036 rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
23037 }
23038 if (updateOffset) {
23039 moment.updateOffset(mom, days || months);
23040 }
23041 }
23042
23043 // check if is an array
23044 function isArray(input) {
23045 return Object.prototype.toString.call(input) === '[object Array]';
23046 }
23047
23048 function isDate(input) {
23049 return Object.prototype.toString.call(input) === '[object Date]' ||
23050 input instanceof Date;
23051 }
23052
23053 // compare two arrays, return the number of differences
23054 function compareArrays(array1, array2, dontConvert) {
23055 var len = Math.min(array1.length, array2.length),
23056 lengthDiff = Math.abs(array1.length - array2.length),
23057 diffs = 0,
23058 i;
23059 for (i = 0; i < len; i++) {
23060 if ((dontConvert && array1[i] !== array2[i]) ||
23061 (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
23062 diffs++;
23063 }
23064 }
23065 return diffs + lengthDiff;
23066 }
23067
23068 function normalizeUnits(units) {
23069 if (units) {
23070 var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
23071 units = unitAliases[units] || camelFunctions[lowered] || lowered;
23072 }
23073 return units;
23074 }
23075
23076 function normalizeObjectUnits(inputObject) {
23077 var normalizedInput = {},
23078 normalizedProp,
23079 prop;
23080
23081 for (prop in inputObject) {
23082 if (hasOwnProp(inputObject, prop)) {
23083 normalizedProp = normalizeUnits(prop);
23084 if (normalizedProp) {
23085 normalizedInput[normalizedProp] = inputObject[prop];
23086 }
23087 }
23088 }
23089
23090 return normalizedInput;
23091 }
23092
23093 function makeList(field) {
23094 var count, setter;
23095
23096 if (field.indexOf('week') === 0) {
23097 count = 7;
23098 setter = 'day';
23099 }
23100 else if (field.indexOf('month') === 0) {
23101 count = 12;
23102 setter = 'month';
23103 }
23104 else {
23105 return;
23106 }
23107
23108 moment[field] = function (format, index) {
23109 var i, getter,
23110 method = moment._locale[field],
23111 results = [];
23112
23113 if (typeof format === 'number') {
23114 index = format;
23115 format = undefined;
23116 }
23117
23118 getter = function (i) {
23119 var m = moment().utc().set(setter, i);
23120 return method.call(moment._locale, m, format || '');
23121 };
23122
23123 if (index != null) {
23124 return getter(index);
23125 }
23126 else {
23127 for (i = 0; i < count; i++) {
23128 results.push(getter(i));
23129 }
23130 return results;
23131 }
23132 };
23133 }
23134
23135 function toInt(argumentForCoercion) {
23136 var coercedNumber = +argumentForCoercion,
23137 value = 0;
23138
23139 if (coercedNumber !== 0 && isFinite(coercedNumber)) {
23140 if (coercedNumber >= 0) {
23141 value = Math.floor(coercedNumber);
23142 } else {
23143 value = Math.ceil(coercedNumber);
23144 }
23145 }
23146
23147 return value;
23148 }
23149
23150 function daysInMonth(year, month) {
23151 return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
23152 }
23153
23154 function weeksInYear(year, dow, doy) {
23155 return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
23156 }
23157
23158 function daysInYear(year) {
23159 return isLeapYear(year) ? 366 : 365;
23160 }
23161
23162 function isLeapYear(year) {
23163 return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
23164 }
23165
23166 function checkOverflow(m) {
23167 var overflow;
23168 if (m._a && m._pf.overflow === -2) {
23169 overflow =
23170 m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
23171 m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
23172 m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
23173 m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
23174 m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
23175 m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
23176 -1;
23177
23178 if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
23179 overflow = DATE;
23180 }
23181
23182 m._pf.overflow = overflow;
23183 }
23184 }
23185
23186 function isValid(m) {
23187 if (m._isValid == null) {
23188 m._isValid = !isNaN(m._d.getTime()) &&
23189 m._pf.overflow < 0 &&
23190 !m._pf.empty &&
23191 !m._pf.invalidMonth &&
23192 !m._pf.nullInput &&
23193 !m._pf.invalidFormat &&
23194 !m._pf.userInvalidated;
23195
23196 if (m._strict) {
23197 m._isValid = m._isValid &&
23198 m._pf.charsLeftOver === 0 &&
23199 m._pf.unusedTokens.length === 0;
23200 }
23201 }
23202 return m._isValid;
23203 }
23204
23205 function normalizeLocale(key) {
23206 return key ? key.toLowerCase().replace('_', '-') : key;
23207 }
23208
23209 // pick the locale from the array
23210 // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
23211 // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
23212 function chooseLocale(names) {
23213 var i = 0, j, next, locale, split;
23214
23215 while (i < names.length) {
23216 split = normalizeLocale(names[i]).split('-');
23217 j = split.length;
23218 next = normalizeLocale(names[i + 1]);
23219 next = next ? next.split('-') : null;
23220 while (j > 0) {
23221 locale = loadLocale(split.slice(0, j).join('-'));
23222 if (locale) {
23223 return locale;
23224 }
23225 if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
23226 //the next array item is better than a shallower substring of this one
23227 break;
23228 }
23229 j--;
23230 }
23231 i++;
23232 }
23233 return null;
23234 }
23235
23236 function loadLocale(name) {
23237 var oldLocale = null;
23238 if (!locales[name] && hasModule) {
23239 try {
23240 oldLocale = moment.locale();
23241 !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }());
23242 // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales
23243 moment.locale(oldLocale);
23244 } catch (e) { }
23245 }
23246 return locales[name];
23247 }
23248
23249 // Return a moment from input, that is local/utc/zone equivalent to model.
23250 function makeAs(input, model) {
23251 return model._isUTC ? moment(input).zone(model._offset || 0) :
23252 moment(input).local();
23253 }
23254
23255 /************************************
23256 Locale
23257 ************************************/
23258
23259
23260 extend(Locale.prototype, {
23261
23262 set : function (config) {
23263 var prop, i;
23264 for (i in config) {
23265 prop = config[i];
23266 if (typeof prop === 'function') {
23267 this[i] = prop;
23268 } else {
23269 this['_' + i] = prop;
23270 }
23271 }
23272 },
23273
23274 _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
23275 months : function (m) {
23276 return this._months[m.month()];
23277 },
23278
23279 _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
23280 monthsShort : function (m) {
23281 return this._monthsShort[m.month()];
23282 },
23283
23284 monthsParse : function (monthName) {
23285 var i, mom, regex;
23286
23287 if (!this._monthsParse) {
23288 this._monthsParse = [];
23289 }
23290
23291 for (i = 0; i < 12; i++) {
23292 // make the regex if we don't have it already
23293 if (!this._monthsParse[i]) {
23294 mom = moment.utc([2000, i]);
23295 regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
23296 this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
23297 }
23298 // test the regex
23299 if (this._monthsParse[i].test(monthName)) {
23300 return i;
23301 }
23302 }
23303 },
23304
23305 _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
23306 weekdays : function (m) {
23307 return this._weekdays[m.day()];
23308 },
23309
23310 _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
23311 weekdaysShort : function (m) {
23312 return this._weekdaysShort[m.day()];
23313 },
23314
23315 _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
23316 weekdaysMin : function (m) {
23317 return this._weekdaysMin[m.day()];
23318 },
23319
23320 weekdaysParse : function (weekdayName) {
23321 var i, mom, regex;
23322
23323 if (!this._weekdaysParse) {
23324 this._weekdaysParse = [];
23325 }
23326
23327 for (i = 0; i < 7; i++) {
23328 // make the regex if we don't have it already
23329 if (!this._weekdaysParse[i]) {
23330 mom = moment([2000, 1]).day(i);
23331 regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
23332 this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
23333 }
23334 // test the regex
23335 if (this._weekdaysParse[i].test(weekdayName)) {
23336 return i;
23337 }
23338 }
23339 },
23340
23341 _longDateFormat : {
23342 LT : 'h:mm A',
23343 L : 'MM/DD/YYYY',
23344 LL : 'MMMM D, YYYY',
23345 LLL : 'MMMM D, YYYY LT',
23346 LLLL : 'dddd, MMMM D, YYYY LT'
23347 },
23348 longDateFormat : function (key) {
23349 var output = this._longDateFormat[key];
23350 if (!output && this._longDateFormat[key.toUpperCase()]) {
23351 output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
23352 return val.slice(1);
23353 });
23354 this._longDateFormat[key] = output;
23355 }
23356 return output;
23357 },
23358
23359 isPM : function (input) {
23360 // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
23361 // Using charAt should be more compatible.
23362 return ((input + '').toLowerCase().charAt(0) === 'p');
23363 },
23364
23365 _meridiemParse : /[ap]\.?m?\.?/i,
23366 meridiem : function (hours, minutes, isLower) {
23367 if (hours > 11) {
23368 return isLower ? 'pm' : 'PM';
23369 } else {
23370 return isLower ? 'am' : 'AM';
23371 }
23372 },
23373
23374 _calendar : {
23375 sameDay : '[Today at] LT',
23376 nextDay : '[Tomorrow at] LT',
23377 nextWeek : 'dddd [at] LT',
23378 lastDay : '[Yesterday at] LT',
23379 lastWeek : '[Last] dddd [at] LT',
23380 sameElse : 'L'
23381 },
23382 calendar : function (key, mom) {
23383 var output = this._calendar[key];
23384 return typeof output === 'function' ? output.apply(mom) : output;
23385 },
23386
23387 _relativeTime : {
23388 future : 'in %s',
23389 past : '%s ago',
23390 s : 'a few seconds',
23391 m : 'a minute',
23392 mm : '%d minutes',
23393 h : 'an hour',
23394 hh : '%d hours',
23395 d : 'a day',
23396 dd : '%d days',
23397 M : 'a month',
23398 MM : '%d months',
23399 y : 'a year',
23400 yy : '%d years'
23401 },
23402
23403 relativeTime : function (number, withoutSuffix, string, isFuture) {
23404 var output = this._relativeTime[string];
23405 return (typeof output === 'function') ?
23406 output(number, withoutSuffix, string, isFuture) :
23407 output.replace(/%d/i, number);
23408 },
23409
23410 pastFuture : function (diff, output) {
23411 var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
23412 return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
23413 },
23414
23415 ordinal : function (number) {
23416 return this._ordinal.replace('%d', number);
23417 },
23418 _ordinal : '%d',
23419
23420 preparse : function (string) {
23421 return string;
23422 },
23423
23424 postformat : function (string) {
23425 return string;
23426 },
23427
23428 week : function (mom) {
23429 return weekOfYear(mom, this._week.dow, this._week.doy).week;
23430 },
23431
23432 _week : {
23433 dow : 0, // Sunday is the first day of the week.
23434 doy : 6 // The week that contains Jan 1st is the first week of the year.
23435 },
23436
23437 _invalidDate: 'Invalid date',
23438 invalidDate: function () {
23439 return this._invalidDate;
23440 }
23441 });
23442
23443 /************************************
23444 Formatting
23445 ************************************/
23446
23447
23448 function removeFormattingTokens(input) {
23449 if (input.match(/\[[\s\S]/)) {
23450 return input.replace(/^\[|\]$/g, '');
23451 }
23452 return input.replace(/\\/g, '');
23453 }
23454
23455 function makeFormatFunction(format) {
23456 var array = format.match(formattingTokens), i, length;
23457
23458 for (i = 0, length = array.length; i < length; i++) {
23459 if (formatTokenFunctions[array[i]]) {
23460 array[i] = formatTokenFunctions[array[i]];
23461 } else {
23462 array[i] = removeFormattingTokens(array[i]);
23463 }
23464 }
23465
23466 return function (mom) {
23467 var output = '';
23468 for (i = 0; i < length; i++) {
23469 output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
23470 }
23471 return output;
23472 };
23473 }
23474
23475 // format date using native date object
23476 function formatMoment(m, format) {
23477 if (!m.isValid()) {
23478 return m.localeData().invalidDate();
23479 }
23480
23481 format = expandFormat(format, m.localeData());
23482
23483 if (!formatFunctions[format]) {
23484 formatFunctions[format] = makeFormatFunction(format);
23485 }
23486
23487 return formatFunctions[format](m);
23488 }
23489
23490 function expandFormat(format, locale) {
23491 var i = 5;
23492
23493 function replaceLongDateFormatTokens(input) {
23494 return locale.longDateFormat(input) || input;
23495 }
23496
23497 localFormattingTokens.lastIndex = 0;
23498 while (i >= 0 && localFormattingTokens.test(format)) {
23499 format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
23500 localFormattingTokens.lastIndex = 0;
23501 i -= 1;
23502 }
23503
23504 return format;
23505 }
23506
23507
23508 /************************************
23509 Parsing
23510 ************************************/
23511
23512
23513 // get the regex to find the next token
23514 function getParseRegexForToken(token, config) {
23515 var a, strict = config._strict;
23516 switch (token) {
23517 case 'Q':
23518 return parseTokenOneDigit;
23519 case 'DDDD':
23520 return parseTokenThreeDigits;
23521 case 'YYYY':
23522 case 'GGGG':
23523 case 'gggg':
23524 return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
23525 case 'Y':
23526 case 'G':
23527 case 'g':
23528 return parseTokenSignedNumber;
23529 case 'YYYYYY':
23530 case 'YYYYY':
23531 case 'GGGGG':
23532 case 'ggggg':
23533 return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
23534 case 'S':
23535 if (strict) {
23536 return parseTokenOneDigit;
23537 }
23538 /* falls through */
23539 case 'SS':
23540 if (strict) {
23541 return parseTokenTwoDigits;
23542 }
23543 /* falls through */
23544 case 'SSS':
23545 if (strict) {
23546 return parseTokenThreeDigits;
23547 }
23548 /* falls through */
23549 case 'DDD':
23550 return parseTokenOneToThreeDigits;
23551 case 'MMM':
23552 case 'MMMM':
23553 case 'dd':
23554 case 'ddd':
23555 case 'dddd':
23556 return parseTokenWord;
23557 case 'a':
23558 case 'A':
23559 return config._locale._meridiemParse;
23560 case 'X':
23561 return parseTokenTimestampMs;
23562 case 'Z':
23563 case 'ZZ':
23564 return parseTokenTimezone;
23565 case 'T':
23566 return parseTokenT;
23567 case 'SSSS':
23568 return parseTokenDigits;
23569 case 'MM':
23570 case 'DD':
23571 case 'YY':
23572 case 'GG':
23573 case 'gg':
23574 case 'HH':
23575 case 'hh':
23576 case 'mm':
23577 case 'ss':
23578 case 'ww':
23579 case 'WW':
23580 return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
23581 case 'M':
23582 case 'D':
23583 case 'd':
23584 case 'H':
23585 case 'h':
23586 case 'm':
23587 case 's':
23588 case 'w':
23589 case 'W':
23590 case 'e':
23591 case 'E':
23592 return parseTokenOneOrTwoDigits;
23593 case 'Do':
23594 return parseTokenOrdinal;
23595 default :
23596 a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i'));
23597 return a;
23598 }
23599 }
23600
23601 function timezoneMinutesFromString(string) {
23602 string = string || '';
23603 var possibleTzMatches = (string.match(parseTokenTimezone) || []),
23604 tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
23605 parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
23606 minutes = +(parts[1] * 60) + toInt(parts[2]);
23607
23608 return parts[0] === '+' ? -minutes : minutes;
23609 }
23610
23611 // function to convert string input to date
23612 function addTimeToArrayFromToken(token, input, config) {
23613 var a, datePartArray = config._a;
23614
23615 switch (token) {
23616 // QUARTER
23617 case 'Q':
23618 if (input != null) {
23619 datePartArray[MONTH] = (toInt(input) - 1) * 3;
23620 }
23621 break;
23622 // MONTH
23623 case 'M' : // fall through to MM
23624 case 'MM' :
23625 if (input != null) {
23626 datePartArray[MONTH] = toInt(input) - 1;
23627 }
23628 break;
23629 case 'MMM' : // fall through to MMMM
23630 case 'MMMM' :
23631 a = config._locale.monthsParse(input);
23632 // if we didn't find a month name, mark the date as invalid.
23633 if (a != null) {
23634 datePartArray[MONTH] = a;
23635 } else {
23636 config._pf.invalidMonth = input;
23637 }
23638 break;
23639 // DAY OF MONTH
23640 case 'D' : // fall through to DD
23641 case 'DD' :
23642 if (input != null) {
23643 datePartArray[DATE] = toInt(input);
23644 }
23645 break;
23646 case 'Do' :
23647 if (input != null) {
23648 datePartArray[DATE] = toInt(parseInt(input, 10));
23649 }
23650 break;
23651 // DAY OF YEAR
23652 case 'DDD' : // fall through to DDDD
23653 case 'DDDD' :
23654 if (input != null) {
23655 config._dayOfYear = toInt(input);
23656 }
23657
23658 break;
23659 // YEAR
23660 case 'YY' :
23661 datePartArray[YEAR] = moment.parseTwoDigitYear(input);
23662 break;
23663 case 'YYYY' :
23664 case 'YYYYY' :
23665 case 'YYYYYY' :
23666 datePartArray[YEAR] = toInt(input);
23667 break;
23668 // AM / PM
23669 case 'a' : // fall through to A
23670 case 'A' :
23671 config._isPm = config._locale.isPM(input);
23672 break;
23673 // 24 HOUR
23674 case 'H' : // fall through to hh
23675 case 'HH' : // fall through to hh
23676 case 'h' : // fall through to hh
23677 case 'hh' :
23678 datePartArray[HOUR] = toInt(input);
23679 break;
23680 // MINUTE
23681 case 'm' : // fall through to mm
23682 case 'mm' :
23683 datePartArray[MINUTE] = toInt(input);
23684 break;
23685 // SECOND
23686 case 's' : // fall through to ss
23687 case 'ss' :
23688 datePartArray[SECOND] = toInt(input);
23689 break;
23690 // MILLISECOND
23691 case 'S' :
23692 case 'SS' :
23693 case 'SSS' :
23694 case 'SSSS' :
23695 datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
23696 break;
23697 // UNIX TIMESTAMP WITH MS
23698 case 'X':
23699 config._d = new Date(parseFloat(input) * 1000);
23700 break;
23701 // TIMEZONE
23702 case 'Z' : // fall through to ZZ
23703 case 'ZZ' :
23704 config._useUTC = true;
23705 config._tzm = timezoneMinutesFromString(input);
23706 break;
23707 // WEEKDAY - human
23708 case 'dd':
23709 case 'ddd':
23710 case 'dddd':
23711 a = config._locale.weekdaysParse(input);
23712 // if we didn't get a weekday name, mark the date as invalid
23713 if (a != null) {
23714 config._w = config._w || {};
23715 config._w['d'] = a;
23716 } else {
23717 config._pf.invalidWeekday = input;
23718 }
23719 break;
23720 // WEEK, WEEK DAY - numeric
23721 case 'w':
23722 case 'ww':
23723 case 'W':
23724 case 'WW':
23725 case 'd':
23726 case 'e':
23727 case 'E':
23728 token = token.substr(0, 1);
23729 /* falls through */
23730 case 'gggg':
23731 case 'GGGG':
23732 case 'GGGGG':
23733 token = token.substr(0, 2);
23734 if (input) {
23735 config._w = config._w || {};
23736 config._w[token] = toInt(input);
23737 }
23738 break;
23739 case 'gg':
23740 case 'GG':
23741 config._w = config._w || {};
23742 config._w[token] = moment.parseTwoDigitYear(input);
23743 }
23744 }
23745
23746 function dayOfYearFromWeekInfo(config) {
23747 var w, weekYear, week, weekday, dow, doy, temp;
23748
23749 w = config._w;
23750 if (w.GG != null || w.W != null || w.E != null) {
23751 dow = 1;
23752 doy = 4;
23753
23754 // TODO: We need to take the current isoWeekYear, but that depends on
23755 // how we interpret now (local, utc, fixed offset). So create
23756 // a now version of current config (take local/utc/offset flags, and
23757 // create now).
23758 weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
23759 week = dfl(w.W, 1);
23760 weekday = dfl(w.E, 1);
23761 } else {
23762 dow = config._locale._week.dow;
23763 doy = config._locale._week.doy;
23764
23765 weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
23766 week = dfl(w.w, 1);
23767
23768 if (w.d != null) {
23769 // weekday -- low day numbers are considered next week
23770 weekday = w.d;
23771 if (weekday < dow) {
23772 ++week;
23773 }
23774 } else if (w.e != null) {
23775 // local weekday -- counting starts from begining of week
23776 weekday = w.e + dow;
23777 } else {
23778 // default to begining of week
23779 weekday = dow;
23780 }
23781 }
23782 temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
23783
23784 config._a[YEAR] = temp.year;
23785 config._dayOfYear = temp.dayOfYear;
23786 }
23787
23788 // convert an array to a date.
23789 // the array should mirror the parameters below
23790 // note: all values past the year are optional and will default to the lowest possible value.
23791 // [year, month, day , hour, minute, second, millisecond]
23792 function dateFromConfig(config) {
23793 var i, date, input = [], currentDate, yearToUse;
23794
23795 if (config._d) {
23796 return;
23797 }
23798
23799 currentDate = currentDateArray(config);
23800
23801 //compute day of the year from weeks and weekdays
23802 if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
23803 dayOfYearFromWeekInfo(config);
23804 }
23805
23806 //if the day of the year is set, figure out what it is
23807 if (config._dayOfYear) {
23808 yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
23809
23810 if (config._dayOfYear > daysInYear(yearToUse)) {
23811 config._pf._overflowDayOfYear = true;
23812 }
23813
23814 date = makeUTCDate(yearToUse, 0, config._dayOfYear);
23815 config._a[MONTH] = date.getUTCMonth();
23816 config._a[DATE] = date.getUTCDate();
23817 }
23818
23819 // Default to current date.
23820 // * if no year, month, day of month are given, default to today
23821 // * if day of month is given, default month and year
23822 // * if month is given, default only year
23823 // * if year is given, don't default anything
23824 for (i = 0; i < 3 && config._a[i] == null; ++i) {
23825 config._a[i] = input[i] = currentDate[i];
23826 }
23827
23828 // Zero out whatever was not defaulted, including time
23829 for (; i < 7; i++) {
23830 config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
23831 }
23832
23833 config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
23834 // Apply timezone offset from input. The actual zone can be changed
23835 // with parseZone.
23836 if (config._tzm != null) {
23837 config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
23838 }
23839 }
23840
23841 function dateFromObject(config) {
23842 var normalizedInput;
23843
23844 if (config._d) {
23845 return;
23846 }
23847
23848 normalizedInput = normalizeObjectUnits(config._i);
23849 config._a = [
23850 normalizedInput.year,
23851 normalizedInput.month,
23852 normalizedInput.day,
23853 normalizedInput.hour,
23854 normalizedInput.minute,
23855 normalizedInput.second,
23856 normalizedInput.millisecond
23857 ];
23858
23859 dateFromConfig(config);
23860 }
23861
23862 function currentDateArray(config) {
23863 var now = new Date();
23864 if (config._useUTC) {
23865 return [
23866 now.getUTCFullYear(),
23867 now.getUTCMonth(),
23868 now.getUTCDate()
23869 ];
23870 } else {
23871 return [now.getFullYear(), now.getMonth(), now.getDate()];
23872 }
23873 }
23874
23875 // date from string and format string
23876 function makeDateFromStringAndFormat(config) {
23877 if (config._f === moment.ISO_8601) {
23878 parseISO(config);
23879 return;
23880 }
23881
23882 config._a = [];
23883 config._pf.empty = true;
23884
23885 // This array is used to make a Date, either with `new Date` or `Date.UTC`
23886 var string = '' + config._i,
23887 i, parsedInput, tokens, token, skipped,
23888 stringLength = string.length,
23889 totalParsedInputLength = 0;
23890
23891 tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
23892
23893 for (i = 0; i < tokens.length; i++) {
23894 token = tokens[i];
23895 parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
23896 if (parsedInput) {
23897 skipped = string.substr(0, string.indexOf(parsedInput));
23898 if (skipped.length > 0) {
23899 config._pf.unusedInput.push(skipped);
23900 }
23901 string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
23902 totalParsedInputLength += parsedInput.length;
23903 }
23904 // don't parse if it's not a known token
23905 if (formatTokenFunctions[token]) {
23906 if (parsedInput) {
23907 config._pf.empty = false;
23908 }
23909 else {
23910 config._pf.unusedTokens.push(token);
23911 }
23912 addTimeToArrayFromToken(token, parsedInput, config);
23913 }
23914 else if (config._strict && !parsedInput) {
23915 config._pf.unusedTokens.push(token);
23916 }
23917 }
23918
23919 // add remaining unparsed input length to the string
23920 config._pf.charsLeftOver = stringLength - totalParsedInputLength;
23921 if (string.length > 0) {
23922 config._pf.unusedInput.push(string);
23923 }
23924
23925 // handle am pm
23926 if (config._isPm && config._a[HOUR] < 12) {
23927 config._a[HOUR] += 12;
23928 }
23929 // if is 12 am, change hours to 0
23930 if (config._isPm === false && config._a[HOUR] === 12) {
23931 config._a[HOUR] = 0;
23932 }
23933
23934 dateFromConfig(config);
23935 checkOverflow(config);
23936 }
23937
23938 function unescapeFormat(s) {
23939 return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
23940 return p1 || p2 || p3 || p4;
23941 });
23942 }
23943
23944 // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
23945 function regexpEscape(s) {
23946 return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
23947 }
23948
23949 // date from string and array of format strings
23950 function makeDateFromStringAndArray(config) {
23951 var tempConfig,
23952 bestMoment,
23953
23954 scoreToBeat,
23955 i,
23956 currentScore;
23957
23958 if (config._f.length === 0) {
23959 config._pf.invalidFormat = true;
23960 config._d = new Date(NaN);
23961 return;
23962 }
23963
23964 for (i = 0; i < config._f.length; i++) {
23965 currentScore = 0;
23966 tempConfig = copyConfig({}, config);
23967 if (config._useUTC != null) {
23968 tempConfig._useUTC = config._useUTC;
23969 }
23970 tempConfig._pf = defaultParsingFlags();
23971 tempConfig._f = config._f[i];
23972 makeDateFromStringAndFormat(tempConfig);
23973
23974 if (!isValid(tempConfig)) {
23975 continue;
23976 }
23977
23978 // if there is any input that was not parsed add a penalty for that format
23979 currentScore += tempConfig._pf.charsLeftOver;
23980
23981 //or tokens
23982 currentScore += tempConfig._pf.unusedTokens.length * 10;
23983
23984 tempConfig._pf.score = currentScore;
23985
23986 if (scoreToBeat == null || currentScore < scoreToBeat) {
23987 scoreToBeat = currentScore;
23988 bestMoment = tempConfig;
23989 }
23990 }
23991
23992 extend(config, bestMoment || tempConfig);
23993 }
23994
23995 // date from iso format
23996 function parseISO(config) {
23997 var i, l,
23998 string = config._i,
23999 match = isoRegex.exec(string);
24000
24001 if (match) {
24002 config._pf.iso = true;
24003 for (i = 0, l = isoDates.length; i < l; i++) {
24004 if (isoDates[i][1].exec(string)) {
24005 // match[5] should be 'T' or undefined
24006 config._f = isoDates[i][0] + (match[6] || ' ');
24007 break;
24008 }
24009 }
24010 for (i = 0, l = isoTimes.length; i < l; i++) {
24011 if (isoTimes[i][1].exec(string)) {
24012 config._f += isoTimes[i][0];
24013 break;
24014 }
24015 }
24016 if (string.match(parseTokenTimezone)) {
24017 config._f += 'Z';
24018 }
24019 makeDateFromStringAndFormat(config);
24020 } else {
24021 config._isValid = false;
24022 }
24023 }
24024
24025 // date from iso format or fallback
24026 function makeDateFromString(config) {
24027 parseISO(config);
24028 if (config._isValid === false) {
24029 delete config._isValid;
24030 moment.createFromInputFallback(config);
24031 }
24032 }
24033
24034 function map(arr, fn) {
24035 var res = [], i;
24036 for (i = 0; i < arr.length; ++i) {
24037 res.push(fn(arr[i], i));
24038 }
24039 return res;
24040 }
24041
24042 function makeDateFromInput(config) {
24043 var input = config._i, matched;
24044 if (input === undefined) {
24045 config._d = new Date();
24046 } else if (isDate(input)) {
24047 config._d = new Date(+input);
24048 } else if ((matched = aspNetJsonRegex.exec(input)) !== null) {
24049 config._d = new Date(+matched[1]);
24050 } else if (typeof input === 'string') {
24051 makeDateFromString(config);
24052 } else if (isArray(input)) {
24053 config._a = map(input.slice(0), function (obj) {
24054 return parseInt(obj, 10);
24055 });
24056 dateFromConfig(config);
24057 } else if (typeof(input) === 'object') {
24058 dateFromObject(config);
24059 } else if (typeof(input) === 'number') {
24060 // from milliseconds
24061 config._d = new Date(input);
24062 } else {
24063 moment.createFromInputFallback(config);
24064 }
24065 }
24066
24067 function makeDate(y, m, d, h, M, s, ms) {
24068 //can't just apply() to create a date:
24069 //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
24070 var date = new Date(y, m, d, h, M, s, ms);
24071
24072 //the date constructor doesn't accept years < 1970
24073 if (y < 1970) {
24074 date.setFullYear(y);
24075 }
24076 return date;
24077 }
24078
24079 function makeUTCDate(y) {
24080 var date = new Date(Date.UTC.apply(null, arguments));
24081 if (y < 1970) {
24082 date.setUTCFullYear(y);
24083 }
24084 return date;
24085 }
24086
24087 function parseWeekday(input, locale) {
24088 if (typeof input === 'string') {
24089 if (!isNaN(input)) {
24090 input = parseInt(input, 10);
24091 }
24092 else {
24093 input = locale.weekdaysParse(input);
24094 if (typeof input !== 'number') {
24095 return null;
24096 }
24097 }
24098 }
24099 return input;
24100 }
24101
24102 /************************************
24103 Relative Time
24104 ************************************/
24105
24106
24107 // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
24108 function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
24109 return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
24110 }
24111
24112 function relativeTime(posNegDuration, withoutSuffix, locale) {
24113 var duration = moment.duration(posNegDuration).abs(),
24114 seconds = round(duration.as('s')),
24115 minutes = round(duration.as('m')),
24116 hours = round(duration.as('h')),
24117 days = round(duration.as('d')),
24118 months = round(duration.as('M')),
24119 years = round(duration.as('y')),
24120
24121 args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
24122 minutes === 1 && ['m'] ||
24123 minutes < relativeTimeThresholds.m && ['mm', minutes] ||
24124 hours === 1 && ['h'] ||
24125 hours < relativeTimeThresholds.h && ['hh', hours] ||
24126 days === 1 && ['d'] ||
24127 days < relativeTimeThresholds.d && ['dd', days] ||
24128 months === 1 && ['M'] ||
24129 months < relativeTimeThresholds.M && ['MM', months] ||
24130 years === 1 && ['y'] || ['yy', years];
24131
24132 args[2] = withoutSuffix;
24133 args[3] = +posNegDuration > 0;
24134 args[4] = locale;
24135 return substituteTimeAgo.apply({}, args);
24136 }
24137
24138
24139 /************************************
24140 Week of Year
24141 ************************************/
24142
24143
24144 // firstDayOfWeek 0 = sun, 6 = sat
24145 // the day of the week that starts the week
24146 // (usually sunday or monday)
24147 // firstDayOfWeekOfYear 0 = sun, 6 = sat
24148 // the first week is the week that contains the first
24149 // of this day of the week
24150 // (eg. ISO weeks use thursday (4))
24151 function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
24152 var end = firstDayOfWeekOfYear - firstDayOfWeek,
24153 daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
24154 adjustedMoment;
24155
24156
24157 if (daysToDayOfWeek > end) {
24158 daysToDayOfWeek -= 7;
24159 }
24160
24161 if (daysToDayOfWeek < end - 7) {
24162 daysToDayOfWeek += 7;
24163 }
24164
24165 adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd');
24166 return {
24167 week: Math.ceil(adjustedMoment.dayOfYear() / 7),
24168 year: adjustedMoment.year()
24169 };
24170 }
24171
24172 //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
24173 function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
24174 var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
24175
24176 d = d === 0 ? 7 : d;
24177 weekday = weekday != null ? weekday : firstDayOfWeek;
24178 daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
24179 dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
24180
24181 return {
24182 year: dayOfYear > 0 ? year : year - 1,
24183 dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
24184 };
24185 }
24186
24187 /************************************
24188 Top Level Functions
24189 ************************************/
24190
24191 function makeMoment(config) {
24192 var input = config._i,
24193 format = config._f;
24194
24195 config._locale = config._locale || moment.localeData(config._l);
24196
24197 if (input === null || (format === undefined && input === '')) {
24198 return moment.invalid({nullInput: true});
24199 }
24200
24201 if (typeof input === 'string') {
24202 config._i = input = config._locale.preparse(input);
24203 }
24204
24205 if (moment.isMoment(input)) {
24206 return new Moment(input, true);
24207 } else if (format) {
24208 if (isArray(format)) {
24209 makeDateFromStringAndArray(config);
24210 } else {
24211 makeDateFromStringAndFormat(config);
24212 }
24213 } else {
24214 makeDateFromInput(config);
24215 }
24216
24217 return new Moment(config);
24218 }
24219
24220 moment = function (input, format, locale, strict) {
24221 var c;
24222
24223 if (typeof(locale) === 'boolean') {
24224 strict = locale;
24225 locale = undefined;
24226 }
24227 // object construction must be done this way.
24228 // https://github.com/moment/moment/issues/1423
24229 c = {};
24230 c._isAMomentObject = true;
24231 c._i = input;
24232 c._f = format;
24233 c._l = locale;
24234 c._strict = strict;
24235 c._isUTC = false;
24236 c._pf = defaultParsingFlags();
24237
24238 return makeMoment(c);
24239 };
24240
24241 moment.suppressDeprecationWarnings = false;
24242
24243 moment.createFromInputFallback = deprecate(
24244 'moment construction falls back to js Date. This is ' +
24245 'discouraged and will be removed in upcoming major ' +
24246 'release. Please refer to ' +
24247 'https://github.com/moment/moment/issues/1407 for more info.',
24248 function (config) {
24249 config._d = new Date(config._i);
24250 }
24251 );
24252
24253 // Pick a moment m from moments so that m[fn](other) is true for all
24254 // other. This relies on the function fn to be transitive.
24255 //
24256 // moments should either be an array of moment objects or an array, whose
24257 // first element is an array of moment objects.
24258 function pickBy(fn, moments) {
24259 var res, i;
24260 if (moments.length === 1 && isArray(moments[0])) {
24261 moments = moments[0];
24262 }
24263 if (!moments.length) {
24264 return moment();
24265 }
24266 res = moments[0];
24267 for (i = 1; i < moments.length; ++i) {
24268 if (moments[i][fn](res)) {
24269 res = moments[i];
24270 }
24271 }
24272 return res;
24273 }
24274
24275 moment.min = function () {
24276 var args = [].slice.call(arguments, 0);
24277
24278 return pickBy('isBefore', args);
24279 };
24280
24281 moment.max = function () {
24282 var args = [].slice.call(arguments, 0);
24283
24284 return pickBy('isAfter', args);
24285 };
24286
24287 // creating with utc
24288 moment.utc = function (input, format, locale, strict) {
24289 var c;
24290
24291 if (typeof(locale) === 'boolean') {
24292 strict = locale;
24293 locale = undefined;
24294 }
24295 // object construction must be done this way.
24296 // https://github.com/moment/moment/issues/1423
24297 c = {};
24298 c._isAMomentObject = true;
24299 c._useUTC = true;
24300 c._isUTC = true;
24301 c._l = locale;
24302 c._i = input;
24303 c._f = format;
24304 c._strict = strict;
24305 c._pf = defaultParsingFlags();
24306
24307 return makeMoment(c).utc();
24308 };
24309
24310 // creating with unix timestamp (in seconds)
24311 moment.unix = function (input) {
24312 return moment(input * 1000);
24313 };
24314
24315 // duration
24316 moment.duration = function (input, key) {
24317 var duration = input,
24318 // matching against regexp is expensive, do it on demand
24319 match = null,
24320 sign,
24321 ret,
24322 parseIso,
24323 diffRes;
24324
24325 if (moment.isDuration(input)) {
24326 duration = {
24327 ms: input._milliseconds,
24328 d: input._days,
24329 M: input._months
24330 };
24331 } else if (typeof input === 'number') {
24332 duration = {};
24333 if (key) {
24334 duration[key] = input;
24335 } else {
24336 duration.milliseconds = input;
24337 }
24338 } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
24339 sign = (match[1] === '-') ? -1 : 1;
24340 duration = {
24341 y: 0,
24342 d: toInt(match[DATE]) * sign,
24343 h: toInt(match[HOUR]) * sign,
24344 m: toInt(match[MINUTE]) * sign,
24345 s: toInt(match[SECOND]) * sign,
24346 ms: toInt(match[MILLISECOND]) * sign
24347 };
24348 } else if (!!(match = isoDurationRegex.exec(input))) {
24349 sign = (match[1] === '-') ? -1 : 1;
24350 parseIso = function (inp) {
24351 // We'd normally use ~~inp for this, but unfortunately it also
24352 // converts floats to ints.
24353 // inp may be undefined, so careful calling replace on it.
24354 var res = inp && parseFloat(inp.replace(',', '.'));
24355 // apply sign while we're at it
24356 return (isNaN(res) ? 0 : res) * sign;
24357 };
24358 duration = {
24359 y: parseIso(match[2]),
24360 M: parseIso(match[3]),
24361 d: parseIso(match[4]),
24362 h: parseIso(match[5]),
24363 m: parseIso(match[6]),
24364 s: parseIso(match[7]),
24365 w: parseIso(match[8])
24366 };
24367 } else if (typeof duration === 'object' &&
24368 ('from' in duration || 'to' in duration)) {
24369 diffRes = momentsDifference(moment(duration.from), moment(duration.to));
24370
24371 duration = {};
24372 duration.ms = diffRes.milliseconds;
24373 duration.M = diffRes.months;
24374 }
24375
24376 ret = new Duration(duration);
24377
24378 if (moment.isDuration(input) && hasOwnProp(input, '_locale')) {
24379 ret._locale = input._locale;
24380 }
24381
24382 return ret;
24383 };
24384
24385 // version number
24386 moment.version = VERSION;
24387
24388 // default format
24389 moment.defaultFormat = isoFormat;
24390
24391 // constant that refers to the ISO standard
24392 moment.ISO_8601 = function () {};
24393
24394 // Plugins that add properties should also add the key here (null value),
24395 // so we can properly clone ourselves.
24396 moment.momentProperties = momentProperties;
24397
24398 // This function will be called whenever a moment is mutated.
24399 // It is intended to keep the offset in sync with the timezone.
24400 moment.updateOffset = function () {};
24401
24402 // This function allows you to set a threshold for relative time strings
24403 moment.relativeTimeThreshold = function (threshold, limit) {
24404 if (relativeTimeThresholds[threshold] === undefined) {
24405 return false;
24406 }
24407 if (limit === undefined) {
24408 return relativeTimeThresholds[threshold];
24409 }
24410 relativeTimeThresholds[threshold] = limit;
24411 return true;
24412 };
24413
24414 moment.lang = deprecate(
24415 'moment.lang is deprecated. Use moment.locale instead.',
24416 function (key, value) {
24417 return moment.locale(key, value);
24418 }
24419 );
24420
24421 // This function will load locale and then set the global locale. If
24422 // no arguments are passed in, it will simply return the current global
24423 // locale key.
24424 moment.locale = function (key, values) {
24425 var data;
24426 if (key) {
24427 if (typeof(values) !== 'undefined') {
24428 data = moment.defineLocale(key, values);
24429 }
24430 else {
24431 data = moment.localeData(key);
24432 }
24433
24434 if (data) {
24435 moment.duration._locale = moment._locale = data;
24436 }
24437 }
24438
24439 return moment._locale._abbr;
24440 };
24441
24442 moment.defineLocale = function (name, values) {
24443 if (values !== null) {
24444 values.abbr = name;
24445 if (!locales[name]) {
24446 locales[name] = new Locale();
24447 }
24448 locales[name].set(values);
24449
24450 // backwards compat for now: also set the locale
24451 moment.locale(name);
24452
24453 return locales[name];
24454 } else {
24455 // useful for testing
24456 delete locales[name];
24457 return null;
24458 }
24459 };
24460
24461 moment.langData = deprecate(
24462 'moment.langData is deprecated. Use moment.localeData instead.',
24463 function (key) {
24464 return moment.localeData(key);
24465 }
24466 );
24467
24468 // returns locale data
24469 moment.localeData = function (key) {
24470 var locale;
24471
24472 if (key && key._locale && key._locale._abbr) {
24473 key = key._locale._abbr;
24474 }
24475
24476 if (!key) {
24477 return moment._locale;
24478 }
24479
24480 if (!isArray(key)) {
24481 //short-circuit everything else
24482 locale = loadLocale(key);
24483 if (locale) {
24484 return locale;
24485 }
24486 key = [key];
24487 }
24488
24489 return chooseLocale(key);
24490 };
24491
24492 // compare moment object
24493 moment.isMoment = function (obj) {
24494 return obj instanceof Moment ||
24495 (obj != null && hasOwnProp(obj, '_isAMomentObject'));
24496 };
24497
24498 // for typechecking Duration objects
24499 moment.isDuration = function (obj) {
24500 return obj instanceof Duration;
24501 };
24502
24503 for (i = lists.length - 1; i >= 0; --i) {
24504 makeList(lists[i]);
24505 }
24506
24507 moment.normalizeUnits = function (units) {
24508 return normalizeUnits(units);
24509 };
24510
24511 moment.invalid = function (flags) {
24512 var m = moment.utc(NaN);
24513 if (flags != null) {
24514 extend(m._pf, flags);
24515 }
24516 else {
24517 m._pf.userInvalidated = true;
24518 }
24519
24520 return m;
24521 };
24522
24523 moment.parseZone = function () {
24524 return moment.apply(null, arguments).parseZone();
24525 };
24526
24527 moment.parseTwoDigitYear = function (input) {
24528 return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
24529 };
24530
24531 /************************************
24532 Moment Prototype
24533 ************************************/
24534
24535
24536 extend(moment.fn = Moment.prototype, {
24537
24538 clone : function () {
24539 return moment(this);
24540 },
24541
24542 valueOf : function () {
24543 return +this._d + ((this._offset || 0) * 60000);
24544 },
24545
24546 unix : function () {
24547 return Math.floor(+this / 1000);
24548 },
24549
24550 toString : function () {
24551 return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
24552 },
24553
24554 toDate : function () {
24555 return this._offset ? new Date(+this) : this._d;
24556 },
24557
24558 toISOString : function () {
24559 var m = moment(this).utc();
24560 if (0 < m.year() && m.year() <= 9999) {
24561 return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
24562 } else {
24563 return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
24564 }
24565 },
24566
24567 toArray : function () {
24568 var m = this;
24569 return [
24570 m.year(),
24571 m.month(),
24572 m.date(),
24573 m.hours(),
24574 m.minutes(),
24575 m.seconds(),
24576 m.milliseconds()
24577 ];
24578 },
24579
24580 isValid : function () {
24581 return isValid(this);
24582 },
24583
24584 isDSTShifted : function () {
24585 if (this._a) {
24586 return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
24587 }
24588
24589 return false;
24590 },
24591
24592 parsingFlags : function () {
24593 return extend({}, this._pf);
24594 },
24595
24596 invalidAt: function () {
24597 return this._pf.overflow;
24598 },
24599
24600 utc : function (keepLocalTime) {
24601 return this.zone(0, keepLocalTime);
24602 },
24603
24604 local : function (keepLocalTime) {
24605 if (this._isUTC) {
24606 this.zone(0, keepLocalTime);
24607 this._isUTC = false;
24608
24609 if (keepLocalTime) {
24610 this.add(this._dateTzOffset(), 'm');
24611 }
24612 }
24613 return this;
24614 },
24615
24616 format : function (inputString) {
24617 var output = formatMoment(this, inputString || moment.defaultFormat);
24618 return this.localeData().postformat(output);
24619 },
24620
24621 add : createAdder(1, 'add'),
24622
24623 subtract : createAdder(-1, 'subtract'),
24624
24625 diff : function (input, units, asFloat) {
24626 var that = makeAs(input, this),
24627 zoneDiff = (this.zone() - that.zone()) * 6e4,
24628 diff, output, daysAdjust;
24629
24630 units = normalizeUnits(units);
24631
24632 if (units === 'year' || units === 'month') {
24633 // average number of days in the months in the given dates
24634 diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
24635 // difference in months
24636 output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
24637 // adjust by taking difference in days, average number of days
24638 // and dst in the given months.
24639 daysAdjust = (this - moment(this).startOf('month')) -
24640 (that - moment(that).startOf('month'));
24641 // same as above but with zones, to negate all dst
24642 daysAdjust -= ((this.zone() - moment(this).startOf('month').zone()) -
24643 (that.zone() - moment(that).startOf('month').zone())) * 6e4;
24644 output += daysAdjust / diff;
24645 if (units === 'year') {
24646 output = output / 12;
24647 }
24648 } else {
24649 diff = (this - that);
24650 output = units === 'second' ? diff / 1e3 : // 1000
24651 units === 'minute' ? diff / 6e4 : // 1000 * 60
24652 units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
24653 units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
24654 units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
24655 diff;
24656 }
24657 return asFloat ? output : absRound(output);
24658 },
24659
24660 from : function (time, withoutSuffix) {
24661 return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
24662 },
24663
24664 fromNow : function (withoutSuffix) {
24665 return this.from(moment(), withoutSuffix);
24666 },
24667
24668 calendar : function (time) {
24669 // We want to compare the start of today, vs this.
24670 // Getting start-of-today depends on whether we're zone'd or not.
24671 var now = time || moment(),
24672 sod = makeAs(now, this).startOf('day'),
24673 diff = this.diff(sod, 'days', true),
24674 format = diff < -6 ? 'sameElse' :
24675 diff < -1 ? 'lastWeek' :
24676 diff < 0 ? 'lastDay' :
24677 diff < 1 ? 'sameDay' :
24678 diff < 2 ? 'nextDay' :
24679 diff < 7 ? 'nextWeek' : 'sameElse';
24680 return this.format(this.localeData().calendar(format, this));
24681 },
24682
24683 isLeapYear : function () {
24684 return isLeapYear(this.year());
24685 },
24686
24687 isDST : function () {
24688 return (this.zone() < this.clone().month(0).zone() ||
24689 this.zone() < this.clone().month(5).zone());
24690 },
24691
24692 day : function (input) {
24693 var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
24694 if (input != null) {
24695 input = parseWeekday(input, this.localeData());
24696 return this.add(input - day, 'd');
24697 } else {
24698 return day;
24699 }
24700 },
24701
24702 month : makeAccessor('Month', true),
24703
24704 startOf : function (units) {
24705 units = normalizeUnits(units);
24706 // the following switch intentionally omits break keywords
24707 // to utilize falling through the cases.
24708 switch (units) {
24709 case 'year':
24710 this.month(0);
24711 /* falls through */
24712 case 'quarter':
24713 case 'month':
24714 this.date(1);
24715 /* falls through */
24716 case 'week':
24717 case 'isoWeek':
24718 case 'day':
24719 this.hours(0);
24720 /* falls through */
24721 case 'hour':
24722 this.minutes(0);
24723 /* falls through */
24724 case 'minute':
24725 this.seconds(0);
24726 /* falls through */
24727 case 'second':
24728 this.milliseconds(0);
24729 /* falls through */
24730 }
24731
24732 // weeks are a special case
24733 if (units === 'week') {
24734 this.weekday(0);
24735 } else if (units === 'isoWeek') {
24736 this.isoWeekday(1);
24737 }
24738
24739 // quarters are also special
24740 if (units === 'quarter') {
24741 this.month(Math.floor(this.month() / 3) * 3);
24742 }
24743
24744 return this;
24745 },
24746
24747 endOf: function (units) {
24748 units = normalizeUnits(units);
24749 return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
24750 },
24751
24752 isAfter: function (input, units) {
24753 units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
24754 if (units === 'millisecond') {
24755 input = moment.isMoment(input) ? input : moment(input);
24756 return +this > +input;
24757 } else {
24758 return +this.clone().startOf(units) > +moment(input).startOf(units);
24759 }
24760 },
24761
24762 isBefore: function (input, units) {
24763 units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
24764 if (units === 'millisecond') {
24765 input = moment.isMoment(input) ? input : moment(input);
24766 return +this < +input;
24767 } else {
24768 return +this.clone().startOf(units) < +moment(input).startOf(units);
24769 }
24770 },
24771
24772 isSame: function (input, units) {
24773 units = normalizeUnits(units || 'millisecond');
24774 if (units === 'millisecond') {
24775 input = moment.isMoment(input) ? input : moment(input);
24776 return +this === +input;
24777 } else {
24778 return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
24779 }
24780 },
24781
24782 min: deprecate(
24783 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548',
24784 function (other) {
24785 other = moment.apply(null, arguments);
24786 return other < this ? this : other;
24787 }
24788 ),
24789
24790 max: deprecate(
24791 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548',
24792 function (other) {
24793 other = moment.apply(null, arguments);
24794 return other > this ? this : other;
24795 }
24796 ),
24797
24798 // keepLocalTime = true means only change the timezone, without
24799 // affecting the local hour. So 5:31:26 +0300 --[zone(2, true)]-->
24800 // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist int zone
24801 // +0200, so we adjust the time as needed, to be valid.
24802 //
24803 // Keeping the time actually adds/subtracts (one hour)
24804 // from the actual represented time. That is why we call updateOffset
24805 // a second time. In case it wants us to change the offset again
24806 // _changeInProgress == true case, then we have to adjust, because
24807 // there is no such time in the given timezone.
24808 zone : function (input, keepLocalTime) {
24809 var offset = this._offset || 0,
24810 localAdjust;
24811 if (input != null) {
24812 if (typeof input === 'string') {
24813 input = timezoneMinutesFromString(input);
24814 }
24815 if (Math.abs(input) < 16) {
24816 input = input * 60;
24817 }
24818 if (!this._isUTC && keepLocalTime) {
24819 localAdjust = this._dateTzOffset();
24820 }
24821 this._offset = input;
24822 this._isUTC = true;
24823 if (localAdjust != null) {
24824 this.subtract(localAdjust, 'm');
24825 }
24826 if (offset !== input) {
24827 if (!keepLocalTime || this._changeInProgress) {
24828 addOrSubtractDurationFromMoment(this,
24829 moment.duration(offset - input, 'm'), 1, false);
24830 } else if (!this._changeInProgress) {
24831 this._changeInProgress = true;
24832 moment.updateOffset(this, true);
24833 this._changeInProgress = null;
24834 }
24835 }
24836 } else {
24837 return this._isUTC ? offset : this._dateTzOffset();
24838 }
24839 return this;
24840 },
24841
24842 zoneAbbr : function () {
24843 return this._isUTC ? 'UTC' : '';
24844 },
24845
24846 zoneName : function () {
24847 return this._isUTC ? 'Coordinated Universal Time' : '';
24848 },
24849
24850 parseZone : function () {
24851 if (this._tzm) {
24852 this.zone(this._tzm);
24853 } else if (typeof this._i === 'string') {
24854 this.zone(this._i);
24855 }
24856 return this;
24857 },
24858
24859 hasAlignedHourOffset : function (input) {
24860 if (!input) {
24861 input = 0;
24862 }
24863 else {
24864 input = moment(input).zone();
24865 }
24866
24867 return (this.zone() - input) % 60 === 0;
24868 },
24869
24870 daysInMonth : function () {
24871 return daysInMonth(this.year(), this.month());
24872 },
24873
24874 dayOfYear : function (input) {
24875 var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
24876 return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
24877 },
24878
24879 quarter : function (input) {
24880 return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
24881 },
24882
24883 weekYear : function (input) {
24884 var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year;
24885 return input == null ? year : this.add((input - year), 'y');
24886 },
24887
24888 isoWeekYear : function (input) {
24889 var year = weekOfYear(this, 1, 4).year;
24890 return input == null ? year : this.add((input - year), 'y');
24891 },
24892
24893 week : function (input) {
24894 var week = this.localeData().week(this);
24895 return input == null ? week : this.add((input - week) * 7, 'd');
24896 },
24897
24898 isoWeek : function (input) {
24899 var week = weekOfYear(this, 1, 4).week;
24900 return input == null ? week : this.add((input - week) * 7, 'd');
24901 },
24902
24903 weekday : function (input) {
24904 var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
24905 return input == null ? weekday : this.add(input - weekday, 'd');
24906 },
24907
24908 isoWeekday : function (input) {
24909 // behaves the same as moment#day except
24910 // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
24911 // as a setter, sunday should belong to the previous week.
24912 return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
24913 },
24914
24915 isoWeeksInYear : function () {
24916 return weeksInYear(this.year(), 1, 4);
24917 },
24918
24919 weeksInYear : function () {
24920 var weekInfo = this.localeData()._week;
24921 return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
24922 },
24923
24924 get : function (units) {
24925 units = normalizeUnits(units);
24926 return this[units]();
24927 },
24928
24929 set : function (units, value) {
24930 units = normalizeUnits(units);
24931 if (typeof this[units] === 'function') {
24932 this[units](value);
24933 }
24934 return this;
24935 },
24936
24937 // If passed a locale key, it will set the locale for this
24938 // instance. Otherwise, it will return the locale configuration
24939 // variables for this instance.
24940 locale : function (key) {
24941 var newLocaleData;
24942
24943 if (key === undefined) {
24944 return this._locale._abbr;
24945 } else {
24946 newLocaleData = moment.localeData(key);
24947 if (newLocaleData != null) {
24948 this._locale = newLocaleData;
24949 }
24950 return this;
24951 }
24952 },
24953
24954 lang : deprecate(
24955 'moment().lang() is deprecated. Use moment().localeData() instead.',
24956 function (key) {
24957 if (key === undefined) {
24958 return this.localeData();
24959 } else {
24960 return this.locale(key);
24961 }
24962 }
24963 ),
24964
24965 localeData : function () {
24966 return this._locale;
24967 },
24968
24969 _dateTzOffset : function () {
24970 // On Firefox.24 Date#getTimezoneOffset returns a floating point.
24971 // https://github.com/moment/moment/pull/1871
24972 return Math.round(this._d.getTimezoneOffset() / 15) * 15;
24973 }
24974 });
24975
24976 function rawMonthSetter(mom, value) {
24977 var dayOfMonth;
24978
24979 // TODO: Move this out of here!
24980 if (typeof value === 'string') {
24981 value = mom.localeData().monthsParse(value);
24982 // TODO: Another silent failure?
24983 if (typeof value !== 'number') {
24984 return mom;
24985 }
24986 }
24987
24988 dayOfMonth = Math.min(mom.date(),
24989 daysInMonth(mom.year(), value));
24990 mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
24991 return mom;
24992 }
24993
24994 function rawGetter(mom, unit) {
24995 return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
24996 }
24997
24998 function rawSetter(mom, unit, value) {
24999 if (unit === 'Month') {
25000 return rawMonthSetter(mom, value);
25001 } else {
25002 return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
25003 }
25004 }
25005
25006 function makeAccessor(unit, keepTime) {
25007 return function (value) {
25008 if (value != null) {
25009 rawSetter(this, unit, value);
25010 moment.updateOffset(this, keepTime);
25011 return this;
25012 } else {
25013 return rawGetter(this, unit);
25014 }
25015 };
25016 }
25017
25018 moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
25019 moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
25020 moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
25021 // Setting the hour should keep the time, because the user explicitly
25022 // specified which hour he wants. So trying to maintain the same hour (in
25023 // a new timezone) makes sense. Adding/subtracting hours does not follow
25024 // this rule.
25025 moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
25026 // moment.fn.month is defined separately
25027 moment.fn.date = makeAccessor('Date', true);
25028 moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true));
25029 moment.fn.year = makeAccessor('FullYear', true);
25030 moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true));
25031
25032 // add plural methods
25033 moment.fn.days = moment.fn.day;
25034 moment.fn.months = moment.fn.month;
25035 moment.fn.weeks = moment.fn.week;
25036 moment.fn.isoWeeks = moment.fn.isoWeek;
25037 moment.fn.quarters = moment.fn.quarter;
25038
25039 // add aliased format methods
25040 moment.fn.toJSON = moment.fn.toISOString;
25041
25042 /************************************
25043 Duration Prototype
25044 ************************************/
25045
25046
25047 function daysToYears (days) {
25048 // 400 years have 146097 days (taking into account leap year rules)
25049 return days * 400 / 146097;
25050 }
25051
25052 function yearsToDays (years) {
25053 // years * 365 + absRound(years / 4) -
25054 // absRound(years / 100) + absRound(years / 400);
25055 return years * 146097 / 400;
25056 }
25057
25058 extend(moment.duration.fn = Duration.prototype, {
25059
25060 _bubble : function () {
25061 var milliseconds = this._milliseconds,
25062 days = this._days,
25063 months = this._months,
25064 data = this._data,
25065 seconds, minutes, hours, years = 0;
25066
25067 // The following code bubbles up values, see the tests for
25068 // examples of what that means.
25069 data.milliseconds = milliseconds % 1000;
25070
25071 seconds = absRound(milliseconds / 1000);
25072 data.seconds = seconds % 60;
25073
25074 minutes = absRound(seconds / 60);
25075 data.minutes = minutes % 60;
25076
25077 hours = absRound(minutes / 60);
25078 data.hours = hours % 24;
25079
25080 days += absRound(hours / 24);
25081
25082 // Accurately convert days to years, assume start from year 0.
25083 years = absRound(daysToYears(days));
25084 days -= absRound(yearsToDays(years));
25085
25086 // 30 days to a month
25087 // TODO (iskren): Use anchor date (like 1st Jan) to compute this.
25088 months += absRound(days / 30);
25089 days %= 30;
25090
25091 // 12 months -> 1 year
25092 years += absRound(months / 12);
25093 months %= 12;
25094
25095 data.days = days;
25096 data.months = months;
25097 data.years = years;
25098 },
25099
25100 abs : function () {
25101 this._milliseconds = Math.abs(this._milliseconds);
25102 this._days = Math.abs(this._days);
25103 this._months = Math.abs(this._months);
25104
25105 this._data.milliseconds = Math.abs(this._data.milliseconds);
25106 this._data.seconds = Math.abs(this._data.seconds);
25107 this._data.minutes = Math.abs(this._data.minutes);
25108 this._data.hours = Math.abs(this._data.hours);
25109 this._data.months = Math.abs(this._data.months);
25110 this._data.years = Math.abs(this._data.years);
25111
25112 return this;
25113 },
25114
25115 weeks : function () {
25116 return absRound(this.days() / 7);
25117 },
25118
25119 valueOf : function () {
25120 return this._milliseconds +
25121 this._days * 864e5 +
25122 (this._months % 12) * 2592e6 +
25123 toInt(this._months / 12) * 31536e6;
25124 },
25125
25126 humanize : function (withSuffix) {
25127 var output = relativeTime(this, !withSuffix, this.localeData());
25128
25129 if (withSuffix) {
25130 output = this.localeData().pastFuture(+this, output);
25131 }
25132
25133 return this.localeData().postformat(output);
25134 },
25135
25136 add : function (input, val) {
25137 // supports only 2.0-style add(1, 's') or add(moment)
25138 var dur = moment.duration(input, val);
25139
25140 this._milliseconds += dur._milliseconds;
25141 this._days += dur._days;
25142 this._months += dur._months;
25143
25144 this._bubble();
25145
25146 return this;
25147 },
25148
25149 subtract : function (input, val) {
25150 var dur = moment.duration(input, val);
25151
25152 this._milliseconds -= dur._milliseconds;
25153 this._days -= dur._days;
25154 this._months -= dur._months;
25155
25156 this._bubble();
25157
25158 return this;
25159 },
25160
25161 get : function (units) {
25162 units = normalizeUnits(units);
25163 return this[units.toLowerCase() + 's']();
25164 },
25165
25166 as : function (units) {
25167 var days, months;
25168 units = normalizeUnits(units);
25169
25170 if (units === 'month' || units === 'year') {
25171 days = this._days + this._milliseconds / 864e5;
25172 months = this._months + daysToYears(days) * 12;
25173 return units === 'month' ? months : months / 12;
25174 } else {
25175 // handle milliseconds separately because of floating point math errors (issue #1867)
25176 days = this._days + yearsToDays(this._months / 12);
25177 switch (units) {
25178 case 'week': return days / 7 + this._milliseconds / 6048e5;
25179 case 'day': return days + this._milliseconds / 864e5;
25180 case 'hour': return days * 24 + this._milliseconds / 36e5;
25181 case 'minute': return days * 24 * 60 + this._milliseconds / 6e4;
25182 case 'second': return days * 24 * 60 * 60 + this._milliseconds / 1000;
25183 // Math.floor prevents floating point math errors here
25184 case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + this._milliseconds;
25185 default: throw new Error('Unknown unit ' + units);
25186 }
25187 }
25188 },
25189
25190 lang : moment.fn.lang,
25191 locale : moment.fn.locale,
25192
25193 toIsoString : deprecate(
25194 'toIsoString() is deprecated. Please use toISOString() instead ' +
25195 '(notice the capitals)',
25196 function () {
25197 return this.toISOString();
25198 }
25199 ),
25200
25201 toISOString : function () {
25202 // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
25203 var years = Math.abs(this.years()),
25204 months = Math.abs(this.months()),
25205 days = Math.abs(this.days()),
25206 hours = Math.abs(this.hours()),
25207 minutes = Math.abs(this.minutes()),
25208 seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
25209
25210 if (!this.asSeconds()) {
25211 // this is the same as C#'s (Noda) and python (isodate)...
25212 // but not other JS (goog.date)
25213 return 'P0D';
25214 }
25215
25216 return (this.asSeconds() < 0 ? '-' : '') +
25217 'P' +
25218 (years ? years + 'Y' : '') +
25219 (months ? months + 'M' : '') +
25220 (days ? days + 'D' : '') +
25221 ((hours || minutes || seconds) ? 'T' : '') +
25222 (hours ? hours + 'H' : '') +
25223 (minutes ? minutes + 'M' : '') +
25224 (seconds ? seconds + 'S' : '');
25225 },
25226
25227 localeData : function () {
25228 return this._locale;
25229 }
25230 });
25231
25232 moment.duration.fn.toString = moment.duration.fn.toISOString;
25233
25234 function makeDurationGetter(name) {
25235 moment.duration.fn[name] = function () {
25236 return this._data[name];
25237 };
25238 }
25239
25240 for (i in unitMillisecondFactors) {
25241 if (hasOwnProp(unitMillisecondFactors, i)) {
25242 makeDurationGetter(i.toLowerCase());
25243 }
25244 }
25245
25246 moment.duration.fn.asMilliseconds = function () {
25247 return this.as('ms');
25248 };
25249 moment.duration.fn.asSeconds = function () {
25250 return this.as('s');
25251 };
25252 moment.duration.fn.asMinutes = function () {
25253 return this.as('m');
25254 };
25255 moment.duration.fn.asHours = function () {
25256 return this.as('h');
25257 };
25258 moment.duration.fn.asDays = function () {
25259 return this.as('d');
25260 };
25261 moment.duration.fn.asWeeks = function () {
25262 return this.as('weeks');
25263 };
25264 moment.duration.fn.asMonths = function () {
25265 return this.as('M');
25266 };
25267 moment.duration.fn.asYears = function () {
25268 return this.as('y');
25269 };
25270
25271 /************************************
25272 Default Locale
25273 ************************************/
25274
25275
25276 // Set default locale, other locale will inherit from English.
25277 moment.locale('en', {
25278 ordinal : function (number) {
25279 var b = number % 10,
25280 output = (toInt(number % 100 / 10) === 1) ? 'th' :
25281 (b === 1) ? 'st' :
25282 (b === 2) ? 'nd' :
25283 (b === 3) ? 'rd' : 'th';
25284 return number + output;
25285 }
25286 });
25287
25288 /* EMBED_LOCALES */
25289
25290 /************************************
25291 Exposing Moment
25292 ************************************/
25293
25294 function makeGlobal(shouldDeprecate) {
25295 /*global ender:false */
25296 if (typeof ender !== 'undefined') {
25297 return;
25298 }
25299 oldGlobalMoment = globalScope.moment;
25300 if (shouldDeprecate) {
25301 globalScope.moment = deprecate(
25302 'Accessing Moment through the global scope is ' +
25303 'deprecated, and will be removed in an upcoming ' +
25304 'release.',
25305 moment);
25306 } else {
25307 globalScope.moment = moment;
25308 }
25309 }
25310
25311 // CommonJS module is defined
25312 if (hasModule) {
25313 module.exports = moment;
25314 } else if (true) {
25315 !(__WEBPACK_AMD_DEFINE_RESULT__ = function (require, exports, module) {
25316 if (module.config && module.config() && module.config().noGlobal === true) {
25317 // release the global variable
25318 globalScope.moment = oldGlobalMoment;
25319 }
25320
25321 return moment;
25322 }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
25323 makeGlobal(true);
25324 } else {
25325 makeGlobal();
25326 }
25327 }).call(this);
25328
25329 /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(65)(module)))
25330
25331/***/ },
25332/* 53 */
25333/***/ function(module, exports, __webpack_require__) {
25334
25335 var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v1.1.3 - 2014-05-20
25336 * http://eightmedia.github.io/hammer.js
25337 *
25338 * Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
25339 * Licensed under the MIT license */
25340
25341 (function(window, undefined) {
25342 'use strict';
25343
25344 /**
25345 * @main
25346 * @module hammer
25347 *
25348 * @class Hammer
25349 * @static
25350 */
25351
25352 /**
25353 * Hammer, use this to create instances
25354 * ````
25355 * var hammertime = new Hammer(myElement);
25356 * ````
25357 *
25358 * @method Hammer
25359 * @param {HTMLElement} element
25360 * @param {Object} [options={}]
25361 * @return {Hammer.Instance}
25362 */
25363 var Hammer = function Hammer(element, options) {
25364 return new Hammer.Instance(element, options || {});
25365 };
25366
25367 /**
25368 * version, as defined in package.json
25369 * the value will be set at each build
25370 * @property VERSION
25371 * @final
25372 * @type {String}
25373 */
25374 Hammer.VERSION = '1.1.3';
25375
25376 /**
25377 * default settings.
25378 * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled
25379 * by setting it's name (like `swipe`) to false.
25380 * You can set the defaults for all instances by changing this object before creating an instance.
25381 * @example
25382 * ````
25383 * Hammer.defaults.drag = false;
25384 * Hammer.defaults.behavior.touchAction = 'pan-y';
25385 * delete Hammer.defaults.behavior.userSelect;
25386 * ````
25387 * @property defaults
25388 * @type {Object}
25389 */
25390 Hammer.defaults = {
25391 /**
25392 * this setting object adds styles and attributes to the element to prevent the browser from doing
25393 * its native behavior. The css properties are auto prefixed for the browsers when needed.
25394 * @property defaults.behavior
25395 * @type {Object}
25396 */
25397 behavior: {
25398 /**
25399 * Disables text selection to improve the dragging gesture. When the value is `none` it also sets
25400 * `onselectstart=false` for IE on the element. Mainly for desktop browsers.
25401 * @property defaults.behavior.userSelect
25402 * @type {String}
25403 * @default 'none'
25404 */
25405 userSelect: 'none',
25406
25407 /**
25408 * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming).
25409 * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event.
25410 * @property defaults.behavior.touchAction
25411 * @type {String}
25412 * @default: 'pan-y'
25413 */
25414 touchAction: 'pan-y',
25415
25416 /**
25417 * Disables the default callout shown when you touch and hold a touch target.
25418 * On iOS, when you touch and hold a touch target such as a link, Safari displays
25419 * a callout containing information about the link. This property allows you to disable that callout.
25420 * @property defaults.behavior.touchCallout
25421 * @type {String}
25422 * @default 'none'
25423 */
25424 touchCallout: 'none',
25425
25426 /**
25427 * Specifies whether zooming is enabled. Used by IE10>
25428 * @property defaults.behavior.contentZooming
25429 * @type {String}
25430 * @default 'none'
25431 */
25432 contentZooming: 'none',
25433
25434 /**
25435 * Specifies that an entire element should be draggable instead of its contents.
25436 * Mainly for desktop browsers.
25437 * @property defaults.behavior.userDrag
25438 * @type {String}
25439 * @default 'none'
25440 */
25441 userDrag: 'none',
25442
25443 /**
25444 * Overrides the highlight color shown when the user taps a link or a JavaScript
25445 * clickable element in Safari on iPhone. This property obeys the alpha value, if specified.
25446 *
25447 * If you don't specify an alpha value, Safari on iPhone applies a default alpha value
25448 * to the color. To disable tap highlighting, set the alpha value to 0 (invisible).
25449 * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped.
25450 * @property defaults.behavior.tapHighlightColor
25451 * @type {String}
25452 * @default 'rgba(0,0,0,0)'
25453 */
25454 tapHighlightColor: 'rgba(0,0,0,0)'
25455 }
25456 };
25457
25458 /**
25459 * hammer document where the base events are added at
25460 * @property DOCUMENT
25461 * @type {HTMLElement}
25462 * @default window.document
25463 */
25464 Hammer.DOCUMENT = document;
25465
25466 /**
25467 * detect support for pointer events
25468 * @property HAS_POINTEREVENTS
25469 * @type {Boolean}
25470 */
25471 Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
25472
25473 /**
25474 * detect support for touch events
25475 * @property HAS_TOUCHEVENTS
25476 * @type {Boolean}
25477 */
25478 Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
25479
25480 /**
25481 * detect mobile browsers
25482 * @property IS_MOBILE
25483 * @type {Boolean}
25484 */
25485 Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent);
25486
25487 /**
25488 * detect if we want to support mouseevents at all
25489 * @property NO_MOUSEEVENTS
25490 * @type {Boolean}
25491 */
25492 Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS;
25493
25494 /**
25495 * interval in which Hammer recalculates current velocity/direction/angle in ms
25496 * @property CALCULATE_INTERVAL
25497 * @type {Number}
25498 * @default 25
25499 */
25500 Hammer.CALCULATE_INTERVAL = 25;
25501
25502 /**
25503 * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup`
25504 * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`)
25505 * @property EVENT_TYPES
25506 * @private
25507 * @writeOnce
25508 * @type {Object}
25509 */
25510 var EVENT_TYPES = {};
25511
25512 /**
25513 * direction strings, for safe comparisons
25514 * @property DIRECTION_DOWN|LEFT|UP|RIGHT
25515 * @final
25516 * @type {String}
25517 * @default 'down' 'left' 'up' 'right'
25518 */
25519 var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
25520 var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
25521 var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
25522 var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
25523
25524 /**
25525 * pointertype strings, for safe comparisons
25526 * @property POINTER_MOUSE|TOUCH|PEN
25527 * @final
25528 * @type {String}
25529 * @default 'mouse' 'touch' 'pen'
25530 */
25531 var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
25532 var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
25533 var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
25534
25535 /**
25536 * eventtypes
25537 * @property EVENT_START|MOVE|END|RELEASE|TOUCH
25538 * @final
25539 * @type {String}
25540 * @default 'start' 'change' 'move' 'end' 'release' 'touch'
25541 */
25542 var EVENT_START = Hammer.EVENT_START = 'start';
25543 var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
25544 var EVENT_END = Hammer.EVENT_END = 'end';
25545 var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release';
25546 var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch';
25547
25548 /**
25549 * if the window events are set...
25550 * @property READY
25551 * @writeOnce
25552 * @type {Boolean}
25553 * @default false
25554 */
25555 Hammer.READY = false;
25556
25557 /**
25558 * plugins namespace
25559 * @property plugins
25560 * @type {Object}
25561 */
25562 Hammer.plugins = Hammer.plugins || {};
25563
25564 /**
25565 * gestures namespace
25566 * see `/gestures` for the definitions
25567 * @property gestures
25568 * @type {Object}
25569 */
25570 Hammer.gestures = Hammer.gestures || {};
25571
25572 /**
25573 * setup events to detect gestures on the document
25574 * this function is called when creating an new instance
25575 * @private
25576 */
25577 function setup() {
25578 if(Hammer.READY) {
25579 return;
25580 }
25581
25582 // find what eventtypes we add listeners to
25583 Event.determineEventTypes();
25584
25585 // Register all gestures inside Hammer.gestures
25586 Utils.each(Hammer.gestures, function(gesture) {
25587 Detection.register(gesture);
25588 });
25589
25590 // Add touch events on the document
25591 Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
25592 Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
25593
25594 // Hammer is ready...!
25595 Hammer.READY = true;
25596 }
25597
25598 /**
25599 * @module hammer
25600 *
25601 * @class Utils
25602 * @static
25603 */
25604 var Utils = Hammer.utils = {
25605 /**
25606 * extend method, could also be used for cloning when `dest` is an empty object.
25607 * changes the dest object
25608 * @method extend
25609 * @param {Object} dest
25610 * @param {Object} src
25611 * @param {Boolean} [merge=false] do a merge
25612 * @return {Object} dest
25613 */
25614 extend: function extend(dest, src, merge) {
25615 for(var key in src) {
25616 if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) {
25617 continue;
25618 }
25619 dest[key] = src[key];
25620 }
25621 return dest;
25622 },
25623
25624 /**
25625 * simple addEventListener wrapper
25626 * @method on
25627 * @param {HTMLElement} element
25628 * @param {String} type
25629 * @param {Function} handler
25630 */
25631 on: function on(element, type, handler) {
25632 element.addEventListener(type, handler, false);
25633 },
25634
25635 /**
25636 * simple removeEventListener wrapper
25637 * @method off
25638 * @param {HTMLElement} element
25639 * @param {String} type
25640 * @param {Function} handler
25641 */
25642 off: function off(element, type, handler) {
25643 element.removeEventListener(type, handler, false);
25644 },
25645
25646 /**
25647 * forEach over arrays and objects
25648 * @method each
25649 * @param {Object|Array} obj
25650 * @param {Function} iterator
25651 * @param {any} iterator.item
25652 * @param {Number} iterator.index
25653 * @param {Object|Array} iterator.obj the source object
25654 * @param {Object} context value to use as `this` in the iterator
25655 */
25656 each: function each(obj, iterator, context) {
25657 var i, len;
25658
25659 // native forEach on arrays
25660 if('forEach' in obj) {
25661 obj.forEach(iterator, context);
25662 // arrays
25663 } else if(obj.length !== undefined) {
25664 for(i = 0, len = obj.length; i < len; i++) {
25665 if(iterator.call(context, obj[i], i, obj) === false) {
25666 return;
25667 }
25668 }
25669 // objects
25670 } else {
25671 for(i in obj) {
25672 if(obj.hasOwnProperty(i) &&
25673 iterator.call(context, obj[i], i, obj) === false) {
25674 return;
25675 }
25676 }
25677 }
25678 },
25679
25680 /**
25681 * find if a string contains the string using indexOf
25682 * @method inStr
25683 * @param {String} src
25684 * @param {String} find
25685 * @return {Boolean} found
25686 */
25687 inStr: function inStr(src, find) {
25688 return src.indexOf(find) > -1;
25689 },
25690
25691 /**
25692 * find if a array contains the object using indexOf or a simple polyfill
25693 * @method inArray
25694 * @param {String} src
25695 * @param {String} find
25696 * @return {Boolean|Number} false when not found, or the index
25697 */
25698 inArray: function inArray(src, find) {
25699 if(src.indexOf) {
25700 var index = src.indexOf(find);
25701 return (index === -1) ? false : index;
25702 } else {
25703 for(var i = 0, len = src.length; i < len; i++) {
25704 if(src[i] === find) {
25705 return i;
25706 }
25707 }
25708 return false;
25709 }
25710 },
25711
25712 /**
25713 * convert an array-like object (`arguments`, `touchlist`) to an array
25714 * @method toArray
25715 * @param {Object} obj
25716 * @return {Array}
25717 */
25718 toArray: function toArray(obj) {
25719 return Array.prototype.slice.call(obj, 0);
25720 },
25721
25722 /**
25723 * find if a node is in the given parent
25724 * @method hasParent
25725 * @param {HTMLElement} node
25726 * @param {HTMLElement} parent
25727 * @return {Boolean} found
25728 */
25729 hasParent: function hasParent(node, parent) {
25730 while(node) {
25731 if(node == parent) {
25732 return true;
25733 }
25734 node = node.parentNode;
25735 }
25736 return false;
25737 },
25738
25739 /**
25740 * get the center of all the touches
25741 * @method getCenter
25742 * @param {Array} touches
25743 * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties
25744 */
25745 getCenter: function getCenter(touches) {
25746 var pageX = [],
25747 pageY = [],
25748 clientX = [],
25749 clientY = [],
25750 min = Math.min,
25751 max = Math.max;
25752
25753 // no need to loop when only one touch
25754 if(touches.length === 1) {
25755 return {
25756 pageX: touches[0].pageX,
25757 pageY: touches[0].pageY,
25758 clientX: touches[0].clientX,
25759 clientY: touches[0].clientY
25760 };
25761 }
25762
25763 Utils.each(touches, function(touch) {
25764 pageX.push(touch.pageX);
25765 pageY.push(touch.pageY);
25766 clientX.push(touch.clientX);
25767 clientY.push(touch.clientY);
25768 });
25769
25770 return {
25771 pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
25772 pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
25773 clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
25774 clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
25775 };
25776 },
25777
25778 /**
25779 * calculate the velocity between two points. unit is in px per ms.
25780 * @method getVelocity
25781 * @param {Number} deltaTime
25782 * @param {Number} deltaX
25783 * @param {Number} deltaY
25784 * @return {Object} velocity `x` and `y`
25785 */
25786 getVelocity: function getVelocity(deltaTime, deltaX, deltaY) {
25787 return {
25788 x: Math.abs(deltaX / deltaTime) || 0,
25789 y: Math.abs(deltaY / deltaTime) || 0
25790 };
25791 },
25792
25793 /**
25794 * calculate the angle between two coordinates
25795 * @method getAngle
25796 * @param {Touch} touch1
25797 * @param {Touch} touch2
25798 * @return {Number} angle
25799 */
25800 getAngle: function getAngle(touch1, touch2) {
25801 var x = touch2.clientX - touch1.clientX,
25802 y = touch2.clientY - touch1.clientY;
25803
25804 return Math.atan2(y, x) * 180 / Math.PI;
25805 },
25806
25807 /**
25808 * do a small comparision to get the direction between two touches.
25809 * @method getDirection
25810 * @param {Touch} touch1
25811 * @param {Touch} touch2
25812 * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN`
25813 */
25814 getDirection: function getDirection(touch1, touch2) {
25815 var x = Math.abs(touch1.clientX - touch2.clientX),
25816 y = Math.abs(touch1.clientY - touch2.clientY);
25817
25818 if(x >= y) {
25819 return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
25820 }
25821 return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
25822 },
25823
25824 /**
25825 * calculate the distance between two touches
25826 * @method getDistance
25827 * @param {Touch}touch1
25828 * @param {Touch} touch2
25829 * @return {Number} distance
25830 */
25831 getDistance: function getDistance(touch1, touch2) {
25832 var x = touch2.clientX - touch1.clientX,
25833 y = touch2.clientY - touch1.clientY;
25834
25835 return Math.sqrt((x * x) + (y * y));
25836 },
25837
25838 /**
25839 * calculate the scale factor between two touchLists
25840 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
25841 * @method getScale
25842 * @param {Array} start array of touches
25843 * @param {Array} end array of touches
25844 * @return {Number} scale
25845 */
25846 getScale: function getScale(start, end) {
25847 // need two fingers...
25848 if(start.length >= 2 && end.length >= 2) {
25849 return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
25850 }
25851 return 1;
25852 },
25853
25854 /**
25855 * calculate the rotation degrees between two touchLists
25856 * @method getRotation
25857 * @param {Array} start array of touches
25858 * @param {Array} end array of touches
25859 * @return {Number} rotation
25860 */
25861 getRotation: function getRotation(start, end) {
25862 // need two fingers
25863 if(start.length >= 2 && end.length >= 2) {
25864 return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
25865 }
25866 return 0;
25867 },
25868
25869 /**
25870 * find out if the direction is vertical *
25871 * @method isVertical
25872 * @param {String} direction matches `DIRECTION_UP|DOWN`
25873 * @return {Boolean} is_vertical
25874 */
25875 isVertical: function isVertical(direction) {
25876 return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
25877 },
25878
25879 /**
25880 * set css properties with their prefixes
25881 * @param {HTMLElement} element
25882 * @param {String} prop
25883 * @param {String} value
25884 * @param {Boolean} [toggle=true]
25885 * @return {Boolean}
25886 */
25887 setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) {
25888 var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms'];
25889 prop = Utils.toCamelCase(prop);
25890
25891 for(var i = 0; i < prefixes.length; i++) {
25892 var p = prop;
25893 // prefixes
25894 if(prefixes[i]) {
25895 p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1);
25896 }
25897
25898 // test the style
25899 if(p in element.style) {
25900 element.style[p] = (toggle == null || toggle) && value || '';
25901 break;
25902 }
25903 }
25904 },
25905
25906 /**
25907 * toggle browser default behavior by setting css properties.
25908 * `userSelect='none'` also sets `element.onselectstart` to false
25909 * `userDrag='none'` also sets `element.ondragstart` to false
25910 *
25911 * @method toggleBehavior
25912 * @param {HtmlElement} element
25913 * @param {Object} props
25914 * @param {Boolean} [toggle=true]
25915 */
25916 toggleBehavior: function toggleBehavior(element, props, toggle) {
25917 if(!props || !element || !element.style) {
25918 return;
25919 }
25920
25921 // set the css properties
25922 Utils.each(props, function(value, prop) {
25923 Utils.setPrefixedCss(element, prop, value, toggle);
25924 });
25925
25926 var falseFn = toggle && function() {
25927 return false;
25928 };
25929
25930 // also the disable onselectstart
25931 if(props.userSelect == 'none') {
25932 element.onselectstart = falseFn;
25933 }
25934 // and disable ondragstart
25935 if(props.userDrag == 'none') {
25936 element.ondragstart = falseFn;
25937 }
25938 },
25939
25940 /**
25941 * convert a string with underscores to camelCase
25942 * so prevent_default becomes preventDefault
25943 * @param {String} str
25944 * @return {String} camelCaseStr
25945 */
25946 toCamelCase: function toCamelCase(str) {
25947 return str.replace(/[_-]([a-z])/g, function(s) {
25948 return s[1].toUpperCase();
25949 });
25950 }
25951 };
25952
25953
25954 /**
25955 * @module hammer
25956 */
25957 /**
25958 * @class Event
25959 * @static
25960 */
25961 var Event = Hammer.event = {
25962 /**
25963 * when touch events have been fired, this is true
25964 * this is used to stop mouse events
25965 * @property prevent_mouseevents
25966 * @private
25967 * @type {Boolean}
25968 */
25969 preventMouseEvents: false,
25970
25971 /**
25972 * if EVENT_START has been fired
25973 * @property started
25974 * @private
25975 * @type {Boolean}
25976 */
25977 started: false,
25978
25979 /**
25980 * when the mouse is hold down, this is true
25981 * @property should_detect
25982 * @private
25983 * @type {Boolean}
25984 */
25985 shouldDetect: false,
25986
25987 /**
25988 * simple event binder with a hook and support for multiple types
25989 * @method on
25990 * @param {HTMLElement} element
25991 * @param {String} type
25992 * @param {Function} handler
25993 * @param {Function} [hook]
25994 * @param {Object} hook.type
25995 */
25996 on: function on(element, type, handler, hook) {
25997 var types = type.split(' ');
25998 Utils.each(types, function(type) {
25999 Utils.on(element, type, handler);
26000 hook && hook(type);
26001 });
26002 },
26003
26004 /**
26005 * simple event unbinder with a hook and support for multiple types
26006 * @method off
26007 * @param {HTMLElement} element
26008 * @param {String} type
26009 * @param {Function} handler
26010 * @param {Function} [hook]
26011 * @param {Object} hook.type
26012 */
26013 off: function off(element, type, handler, hook) {
26014 var types = type.split(' ');
26015 Utils.each(types, function(type) {
26016 Utils.off(element, type, handler);
26017 hook && hook(type);
26018 });
26019 },
26020
26021 /**
26022 * the core touch event handler.
26023 * this finds out if we should to detect gestures
26024 * @method onTouch
26025 * @param {HTMLElement} element
26026 * @param {String} eventType matches `EVENT_START|MOVE|END`
26027 * @param {Function} handler
26028 * @return onTouchHandler {Function} the core event handler
26029 */
26030 onTouch: function onTouch(element, eventType, handler) {
26031 var self = this;
26032
26033 var onTouchHandler = function onTouchHandler(ev) {
26034 var srcType = ev.type.toLowerCase(),
26035 isPointer = Hammer.HAS_POINTEREVENTS,
26036 isMouse = Utils.inStr(srcType, 'mouse'),
26037 triggerType;
26038
26039 // if we are in a mouseevent, but there has been a touchevent triggered in this session
26040 // we want to do nothing. simply break out of the event.
26041 if(isMouse && self.preventMouseEvents) {
26042 return;
26043
26044 // mousebutton must be down
26045 } else if(isMouse && eventType == EVENT_START && ev.button === 0) {
26046 self.preventMouseEvents = false;
26047 self.shouldDetect = true;
26048 } else if(isPointer && eventType == EVENT_START) {
26049 self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev));
26050 // just a valid start event, but no mouse
26051 } else if(!isMouse && eventType == EVENT_START) {
26052 self.preventMouseEvents = true;
26053 self.shouldDetect = true;
26054 }
26055
26056 // update the pointer event before entering the detection
26057 if(isPointer && eventType != EVENT_END) {
26058 PointerEvent.updatePointer(eventType, ev);
26059 }
26060
26061 // we are in a touch/down state, so allowed detection of gestures
26062 if(self.shouldDetect) {
26063 triggerType = self.doDetect.call(self, ev, eventType, element, handler);
26064 }
26065
26066 // ...and we are done with the detection
26067 // so reset everything to start each detection totally fresh
26068 if(triggerType == EVENT_END) {
26069 self.preventMouseEvents = false;
26070 self.shouldDetect = false;
26071 PointerEvent.reset();
26072 // update the pointerevent object after the detection
26073 }
26074
26075 if(isPointer && eventType == EVENT_END) {
26076 PointerEvent.updatePointer(eventType, ev);
26077 }
26078 };
26079
26080 this.on(element, EVENT_TYPES[eventType], onTouchHandler);
26081 return onTouchHandler;
26082 },
26083
26084 /**
26085 * the core detection method
26086 * this finds out what hammer-touch-events to trigger
26087 * @method doDetect
26088 * @param {Object} ev
26089 * @param {String} eventType matches `EVENT_START|MOVE|END`
26090 * @param {HTMLElement} element
26091 * @param {Function} handler
26092 * @return {String} triggerType matches `EVENT_START|MOVE|END`
26093 */
26094 doDetect: function doDetect(ev, eventType, element, handler) {
26095 var touchList = this.getTouchList(ev, eventType);
26096 var touchListLength = touchList.length;
26097 var triggerType = eventType;
26098 var triggerChange = touchList.trigger; // used by fakeMultitouch plugin
26099 var changedLength = touchListLength;
26100
26101 // at each touchstart-like event we want also want to trigger a TOUCH event...
26102 if(eventType == EVENT_START) {
26103 triggerChange = EVENT_TOUCH;
26104 // ...the same for a touchend-like event
26105 } else if(eventType == EVENT_END) {
26106 triggerChange = EVENT_RELEASE;
26107
26108 // keep track of how many touches have been removed
26109 changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1);
26110 }
26111
26112 // after there are still touches on the screen,
26113 // we just want to trigger a MOVE event. so change the START or END to a MOVE
26114 // but only after detection has been started, the first time we actualy want a START
26115 if(changedLength > 0 && this.started) {
26116 triggerType = EVENT_MOVE;
26117 }
26118
26119 // detection has been started, we keep track of this, see above
26120 this.started = true;
26121
26122 // generate some event data, some basic information
26123 var evData = this.collectEventData(element, triggerType, touchList, ev);
26124
26125 // trigger the triggerType event before the change (TOUCH, RELEASE) events
26126 // but the END event should be at last
26127 if(eventType != EVENT_END) {
26128 handler.call(Detection, evData);
26129 }
26130
26131 // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed
26132 if(triggerChange) {
26133 evData.changedLength = changedLength;
26134 evData.eventType = triggerChange;
26135
26136 handler.call(Detection, evData);
26137
26138 evData.eventType = triggerType;
26139 delete evData.changedLength;
26140 }
26141
26142 // trigger the END event
26143 if(triggerType == EVENT_END) {
26144 handler.call(Detection, evData);
26145
26146 // ...and we are done with the detection
26147 // so reset everything to start each detection totally fresh
26148 this.started = false;
26149 }
26150
26151 return triggerType;
26152 },
26153
26154 /**
26155 * we have different events for each device/browser
26156 * determine what we need and set them in the EVENT_TYPES constant
26157 * the `onTouch` method is bind to these properties.
26158 * @method determineEventTypes
26159 * @return {Object} events
26160 */
26161 determineEventTypes: function determineEventTypes() {
26162 var types;
26163 if(Hammer.HAS_POINTEREVENTS) {
26164 if(window.PointerEvent) {
26165 types = [
26166 'pointerdown',
26167 'pointermove',
26168 'pointerup pointercancel lostpointercapture'
26169 ];
26170 } else {
26171 types = [
26172 'MSPointerDown',
26173 'MSPointerMove',
26174 'MSPointerUp MSPointerCancel MSLostPointerCapture'
26175 ];
26176 }
26177 } else if(Hammer.NO_MOUSEEVENTS) {
26178 types = [
26179 'touchstart',
26180 'touchmove',
26181 'touchend touchcancel'
26182 ];
26183 } else {
26184 types = [
26185 'touchstart mousedown',
26186 'touchmove mousemove',
26187 'touchend touchcancel mouseup'
26188 ];
26189 }
26190
26191 EVENT_TYPES[EVENT_START] = types[0];
26192 EVENT_TYPES[EVENT_MOVE] = types[1];
26193 EVENT_TYPES[EVENT_END] = types[2];
26194 return EVENT_TYPES;
26195 },
26196
26197 /**
26198 * create touchList depending on the event
26199 * @method getTouchList
26200 * @param {Object} ev
26201 * @param {String} eventType
26202 * @return {Array} touches
26203 */
26204 getTouchList: function getTouchList(ev, eventType) {
26205 // get the fake pointerEvent touchlist
26206 if(Hammer.HAS_POINTEREVENTS) {
26207 return PointerEvent.getTouchList();
26208 }
26209
26210 // get the touchlist
26211 if(ev.touches) {
26212 if(eventType == EVENT_MOVE) {
26213 return ev.touches;
26214 }
26215
26216 var identifiers = [];
26217 var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches));
26218 var touchList = [];
26219
26220 Utils.each(concat, function(touch) {
26221 if(Utils.inArray(identifiers, touch.identifier) === false) {
26222 touchList.push(touch);
26223 }
26224 identifiers.push(touch.identifier);
26225 });
26226
26227 return touchList;
26228 }
26229
26230 // make fake touchList from mouse position
26231 ev.identifier = 1;
26232 return [ev];
26233 },
26234
26235 /**
26236 * collect basic event data
26237 * @method collectEventData
26238 * @param {HTMLElement} element
26239 * @param {String} eventType matches `EVENT_START|MOVE|END`
26240 * @param {Array} touches
26241 * @param {Object} ev
26242 * @return {Object} ev
26243 */
26244 collectEventData: function collectEventData(element, eventType, touches, ev) {
26245 // find out pointerType
26246 var pointerType = POINTER_TOUCH;
26247 if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
26248 pointerType = POINTER_MOUSE;
26249 } else if(PointerEvent.matchType(POINTER_PEN, ev)) {
26250 pointerType = POINTER_PEN;
26251 }
26252
26253 return {
26254 center: Utils.getCenter(touches),
26255 timeStamp: Date.now(),
26256 target: ev.target,
26257 touches: touches,
26258 eventType: eventType,
26259 pointerType: pointerType,
26260 srcEvent: ev,
26261
26262 /**
26263 * prevent the browser default actions
26264 * mostly used to disable scrolling of the browser
26265 */
26266 preventDefault: function() {
26267 var srcEvent = this.srcEvent;
26268 srcEvent.preventManipulation && srcEvent.preventManipulation();
26269 srcEvent.preventDefault && srcEvent.preventDefault();
26270 },
26271
26272 /**
26273 * stop bubbling the event up to its parents
26274 */
26275 stopPropagation: function() {
26276 this.srcEvent.stopPropagation();
26277 },
26278
26279 /**
26280 * immediately stop gesture detection
26281 * might be useful after a swipe was detected
26282 * @return {*}
26283 */
26284 stopDetect: function() {
26285 return Detection.stopDetect();
26286 }
26287 };
26288 }
26289 };
26290
26291
26292 /**
26293 * @module hammer
26294 *
26295 * @class PointerEvent
26296 * @static
26297 */
26298 var PointerEvent = Hammer.PointerEvent = {
26299 /**
26300 * holds all pointers, by `identifier`
26301 * @property pointers
26302 * @type {Object}
26303 */
26304 pointers: {},
26305
26306 /**
26307 * get the pointers as an array
26308 * @method getTouchList
26309 * @return {Array} touchlist
26310 */
26311 getTouchList: function getTouchList() {
26312 var touchlist = [];
26313 // we can use forEach since pointerEvents only is in IE10
26314 Utils.each(this.pointers, function(pointer) {
26315 touchlist.push(pointer);
26316 });
26317 return touchlist;
26318 },
26319
26320 /**
26321 * update the position of a pointer
26322 * @method updatePointer
26323 * @param {String} eventType matches `EVENT_START|MOVE|END`
26324 * @param {Object} pointerEvent
26325 */
26326 updatePointer: function updatePointer(eventType, pointerEvent) {
26327 if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) {
26328 delete this.pointers[pointerEvent.pointerId];
26329 } else {
26330 pointerEvent.identifier = pointerEvent.pointerId;
26331 this.pointers[pointerEvent.pointerId] = pointerEvent;
26332 }
26333 },
26334
26335 /**
26336 * check if ev matches pointertype
26337 * @method matchType
26338 * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN`
26339 * @param {PointerEvent} ev
26340 */
26341 matchType: function matchType(pointerType, ev) {
26342 if(!ev.pointerType) {
26343 return false;
26344 }
26345
26346 var pt = ev.pointerType,
26347 types = {};
26348
26349 types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE));
26350 types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH));
26351 types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN));
26352 return types[pointerType];
26353 },
26354
26355 /**
26356 * reset the stored pointers
26357 * @method reset
26358 */
26359 reset: function resetList() {
26360 this.pointers = {};
26361 }
26362 };
26363
26364
26365 /**
26366 * @module hammer
26367 *
26368 * @class Detection
26369 * @static
26370 */
26371 var Detection = Hammer.detection = {
26372 // contains all registred Hammer.gestures in the correct order
26373 gestures: [],
26374
26375 // data of the current Hammer.gesture detection session
26376 current: null,
26377
26378 // the previous Hammer.gesture session data
26379 // is a full clone of the previous gesture.current object
26380 previous: null,
26381
26382 // when this becomes true, no gestures are fired
26383 stopped: false,
26384
26385 /**
26386 * start Hammer.gesture detection
26387 * @method startDetect
26388 * @param {Hammer.Instance} inst
26389 * @param {Object} eventData
26390 */
26391 startDetect: function startDetect(inst, eventData) {
26392 // already busy with a Hammer.gesture detection on an element
26393 if(this.current) {
26394 return;
26395 }
26396
26397 this.stopped = false;
26398
26399 // holds current session
26400 this.current = {
26401 inst: inst, // reference to HammerInstance we're working for
26402 startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc
26403 lastEvent: false, // last eventData
26404 lastCalcEvent: false, // last eventData for calculations.
26405 futureCalcEvent: false, // last eventData for calculations.
26406 lastCalcData: {}, // last lastCalcData
26407 name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
26408 };
26409
26410 this.detect(eventData);
26411 },
26412
26413 /**
26414 * Hammer.gesture detection
26415 * @method detect
26416 * @param {Object} eventData
26417 * @return {any}
26418 */
26419 detect: function detect(eventData) {
26420 if(!this.current || this.stopped) {
26421 return;
26422 }
26423
26424 // extend event data with calculations about scale, distance etc
26425 eventData = this.extendEventData(eventData);
26426
26427 // hammer instance and instance options
26428 var inst = this.current.inst,
26429 instOptions = inst.options;
26430
26431 // call Hammer.gesture handlers
26432 Utils.each(this.gestures, function triggerGesture(gesture) {
26433 // only when the instance options have enabled this gesture
26434 if(!this.stopped && inst.enabled && instOptions[gesture.name]) {
26435 gesture.handler.call(gesture, eventData, inst);
26436 }
26437 }, this);
26438
26439 // store as previous event event
26440 if(this.current) {
26441 this.current.lastEvent = eventData;
26442 }
26443
26444 if(eventData.eventType == EVENT_END) {
26445 this.stopDetect();
26446 }
26447
26448 return eventData;
26449 },
26450
26451 /**
26452 * clear the Hammer.gesture vars
26453 * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
26454 * to stop other Hammer.gestures from being fired
26455 * @method stopDetect
26456 */
26457 stopDetect: function stopDetect() {
26458 // clone current data to the store as the previous gesture
26459 // used for the double tap gesture, since this is an other gesture detect session
26460 this.previous = Utils.extend({}, this.current);
26461
26462 // reset the current
26463 this.current = null;
26464 this.stopped = true;
26465 },
26466
26467 /**
26468 * calculate velocity, angle and direction
26469 * @method getVelocityData
26470 * @param {Object} ev
26471 * @param {Object} center
26472 * @param {Number} deltaTime
26473 * @param {Number} deltaX
26474 * @param {Number} deltaY
26475 */
26476 getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) {
26477 var cur = this.current,
26478 recalc = false,
26479 calcEv = cur.lastCalcEvent,
26480 calcData = cur.lastCalcData;
26481
26482 if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) {
26483 center = calcEv.center;
26484 deltaTime = ev.timeStamp - calcEv.timeStamp;
26485 deltaX = ev.center.clientX - calcEv.center.clientX;
26486 deltaY = ev.center.clientY - calcEv.center.clientY;
26487 recalc = true;
26488 }
26489
26490 if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
26491 cur.futureCalcEvent = ev;
26492 }
26493
26494 if(!cur.lastCalcEvent || recalc) {
26495 calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY);
26496 calcData.angle = Utils.getAngle(center, ev.center);
26497 calcData.direction = Utils.getDirection(center, ev.center);
26498
26499 cur.lastCalcEvent = cur.futureCalcEvent || ev;
26500 cur.futureCalcEvent = ev;
26501 }
26502
26503 ev.velocityX = calcData.velocity.x;
26504 ev.velocityY = calcData.velocity.y;
26505 ev.interimAngle = calcData.angle;
26506 ev.interimDirection = calcData.direction;
26507 },
26508
26509 /**
26510 * extend eventData for Hammer.gestures
26511 * @method extendEventData
26512 * @param {Object} ev
26513 * @return {Object} ev
26514 */
26515 extendEventData: function extendEventData(ev) {
26516 var cur = this.current,
26517 startEv = cur.startEvent,
26518 lastEv = cur.lastEvent || startEv;
26519
26520 // update the start touchlist to calculate the scale/rotation
26521 if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) {
26522 startEv.touches = [];
26523 Utils.each(ev.touches, function(touch) {
26524 startEv.touches.push({
26525 clientX: touch.clientX,
26526 clientY: touch.clientY
26527 });
26528 });
26529 }
26530
26531 var deltaTime = ev.timeStamp - startEv.timeStamp,
26532 deltaX = ev.center.clientX - startEv.center.clientX,
26533 deltaY = ev.center.clientY - startEv.center.clientY;
26534
26535 this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY);
26536
26537 Utils.extend(ev, {
26538 startEvent: startEv,
26539
26540 deltaTime: deltaTime,
26541 deltaX: deltaX,
26542 deltaY: deltaY,
26543
26544 distance: Utils.getDistance(startEv.center, ev.center),
26545 angle: Utils.getAngle(startEv.center, ev.center),
26546 direction: Utils.getDirection(startEv.center, ev.center),
26547 scale: Utils.getScale(startEv.touches, ev.touches),
26548 rotation: Utils.getRotation(startEv.touches, ev.touches)
26549 });
26550
26551 return ev;
26552 },
26553
26554 /**
26555 * register new gesture
26556 * @method register
26557 * @param {Object} gesture object, see `gestures/` for documentation
26558 * @return {Array} gestures
26559 */
26560 register: function register(gesture) {
26561 // add an enable gesture options if there is no given
26562 var options = gesture.defaults || {};
26563 if(options[gesture.name] === undefined) {
26564 options[gesture.name] = true;
26565 }
26566
26567 // extend Hammer default options with the Hammer.gesture options
26568 Utils.extend(Hammer.defaults, options, true);
26569
26570 // set its index
26571 gesture.index = gesture.index || 1000;
26572
26573 // add Hammer.gesture to the list
26574 this.gestures.push(gesture);
26575
26576 // sort the list by index
26577 this.gestures.sort(function(a, b) {
26578 if(a.index < b.index) {
26579 return -1;
26580 }
26581 if(a.index > b.index) {
26582 return 1;
26583 }
26584 return 0;
26585 });
26586
26587 return this.gestures;
26588 }
26589 };
26590
26591
26592 /**
26593 * @module hammer
26594 */
26595
26596 /**
26597 * create new hammer instance
26598 * all methods should return the instance itself, so it is chainable.
26599 *
26600 * @class Instance
26601 * @constructor
26602 * @param {HTMLElement} element
26603 * @param {Object} [options={}] options are merged with `Hammer.defaults`
26604 * @return {Hammer.Instance}
26605 */
26606 Hammer.Instance = function(element, options) {
26607 var self = this;
26608
26609 // setup HammerJS window events and register all gestures
26610 // this also sets up the default options
26611 setup();
26612
26613 /**
26614 * @property element
26615 * @type {HTMLElement}
26616 */
26617 this.element = element;
26618
26619 /**
26620 * @property enabled
26621 * @type {Boolean}
26622 * @protected
26623 */
26624 this.enabled = true;
26625
26626 /**
26627 * options, merged with the defaults
26628 * options with an _ are converted to camelCase
26629 * @property options
26630 * @type {Object}
26631 */
26632 Utils.each(options, function(value, name) {
26633 delete options[name];
26634 options[Utils.toCamelCase(name)] = value;
26635 });
26636
26637 this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {});
26638
26639 // add some css to the element to prevent the browser from doing its native behavoir
26640 if(this.options.behavior) {
26641 Utils.toggleBehavior(this.element, this.options.behavior, true);
26642 }
26643
26644 /**
26645 * event start handler on the element to start the detection
26646 * @property eventStartHandler
26647 * @type {Object}
26648 */
26649 this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
26650 if(self.enabled && ev.eventType == EVENT_START) {
26651 Detection.startDetect(self, ev);
26652 } else if(ev.eventType == EVENT_TOUCH) {
26653 Detection.detect(ev);
26654 }
26655 });
26656
26657 /**
26658 * keep a list of user event handlers which needs to be removed when calling 'dispose'
26659 * @property eventHandlers
26660 * @type {Array}
26661 */
26662 this.eventHandlers = [];
26663 };
26664
26665 Hammer.Instance.prototype = {
26666 /**
26667 * bind events to the instance
26668 * @method on
26669 * @chainable
26670 * @param {String} gestures multiple gestures by splitting with a space
26671 * @param {Function} handler
26672 * @param {Object} handler.ev event object
26673 */
26674 on: function onEvent(gestures, handler) {
26675 var self = this;
26676 Event.on(self.element, gestures, handler, function(type) {
26677 self.eventHandlers.push({ gesture: type, handler: handler });
26678 });
26679 return self;
26680 },
26681
26682 /**
26683 * unbind events to the instance
26684 * @method off
26685 * @chainable
26686 * @param {String} gestures
26687 * @param {Function} handler
26688 */
26689 off: function offEvent(gestures, handler) {
26690 var self = this;
26691
26692 Event.off(self.element, gestures, handler, function(type) {
26693 var index = Utils.inArray({ gesture: type, handler: handler });
26694 if(index !== false) {
26695 self.eventHandlers.splice(index, 1);
26696 }
26697 });
26698 return self;
26699 },
26700
26701 /**
26702 * trigger gesture event
26703 * @method trigger
26704 * @chainable
26705 * @param {String} gesture
26706 * @param {Object} [eventData]
26707 */
26708 trigger: function triggerEvent(gesture, eventData) {
26709 // optional
26710 if(!eventData) {
26711 eventData = {};
26712 }
26713
26714 // create DOM event
26715 var event = Hammer.DOCUMENT.createEvent('Event');
26716 event.initEvent(gesture, true, true);
26717 event.gesture = eventData;
26718
26719 // trigger on the target if it is in the instance element,
26720 // this is for event delegation tricks
26721 var element = this.element;
26722 if(Utils.hasParent(eventData.target, element)) {
26723 element = eventData.target;
26724 }
26725
26726 element.dispatchEvent(event);
26727 return this;
26728 },
26729
26730 /**
26731 * enable of disable hammer.js detection
26732 * @method enable
26733 * @chainable
26734 * @param {Boolean} state
26735 */
26736 enable: function enable(state) {
26737 this.enabled = state;
26738 return this;
26739 },
26740
26741 /**
26742 * dispose this hammer instance
26743 * @method dispose
26744 * @return {Null}
26745 */
26746 dispose: function dispose() {
26747 var i, eh;
26748
26749 // undo all changes made by stop_browser_behavior
26750 Utils.toggleBehavior(this.element, this.options.behavior, false);
26751
26752 // unbind all custom event handlers
26753 for(i = -1; (eh = this.eventHandlers[++i]);) {
26754 Utils.off(this.element, eh.gesture, eh.handler);
26755 }
26756
26757 this.eventHandlers = [];
26758
26759 // unbind the start event listener
26760 Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler);
26761
26762 return null;
26763 }
26764 };
26765
26766
26767 /**
26768 * @module gestures
26769 */
26770 /**
26771 * Move with x fingers (default 1) around on the page.
26772 * Preventing the default browser behavior is a good way to improve feel and working.
26773 * ````
26774 * hammertime.on("drag", function(ev) {
26775 * console.log(ev);
26776 * ev.gesture.preventDefault();
26777 * });
26778 * ````
26779 *
26780 * @class Drag
26781 * @static
26782 */
26783 /**
26784 * @event drag
26785 * @param {Object} ev
26786 */
26787 /**
26788 * @event dragstart
26789 * @param {Object} ev
26790 */
26791 /**
26792 * @event dragend
26793 * @param {Object} ev
26794 */
26795 /**
26796 * @event drapleft
26797 * @param {Object} ev
26798 */
26799 /**
26800 * @event dragright
26801 * @param {Object} ev
26802 */
26803 /**
26804 * @event dragup
26805 * @param {Object} ev
26806 */
26807 /**
26808 * @event dragdown
26809 * @param {Object} ev
26810 */
26811
26812 /**
26813 * @param {String} name
26814 */
26815 (function(name) {
26816 var triggered = false;
26817
26818 function dragGesture(ev, inst) {
26819 var cur = Detection.current;
26820
26821 // max touches
26822 if(inst.options.dragMaxTouches > 0 &&
26823 ev.touches.length > inst.options.dragMaxTouches) {
26824 return;
26825 }
26826
26827 switch(ev.eventType) {
26828 case EVENT_START:
26829 triggered = false;
26830 break;
26831
26832 case EVENT_MOVE:
26833 // when the distance we moved is too small we skip this gesture
26834 // or we can be already in dragging
26835 if(ev.distance < inst.options.dragMinDistance &&
26836 cur.name != name) {
26837 return;
26838 }
26839
26840 var startCenter = cur.startEvent.center;
26841
26842 // we are dragging!
26843 if(cur.name != name) {
26844 cur.name = name;
26845 if(inst.options.dragDistanceCorrection && ev.distance > 0) {
26846 // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center.
26847 // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0.
26848 // It might be useful to save the original start point somewhere
26849 var factor = Math.abs(inst.options.dragMinDistance / ev.distance);
26850 startCenter.pageX += ev.deltaX * factor;
26851 startCenter.pageY += ev.deltaY * factor;
26852 startCenter.clientX += ev.deltaX * factor;
26853 startCenter.clientY += ev.deltaY * factor;
26854
26855 // recalculate event data using new start point
26856 ev = Detection.extendEventData(ev);
26857 }
26858 }
26859
26860 // lock drag to axis?
26861 if(cur.lastEvent.dragLockToAxis ||
26862 ( inst.options.dragLockToAxis &&
26863 inst.options.dragLockMinDistance <= ev.distance
26864 )) {
26865 ev.dragLockToAxis = true;
26866 }
26867
26868 // keep direction on the axis that the drag gesture started on
26869 var lastDirection = cur.lastEvent.direction;
26870 if(ev.dragLockToAxis && lastDirection !== ev.direction) {
26871 if(Utils.isVertical(lastDirection)) {
26872 ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
26873 } else {
26874 ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
26875 }
26876 }
26877
26878 // first time, trigger dragstart event
26879 if(!triggered) {
26880 inst.trigger(name + 'start', ev);
26881 triggered = true;
26882 }
26883
26884 // trigger events
26885 inst.trigger(name, ev);
26886 inst.trigger(name + ev.direction, ev);
26887
26888 var isVertical = Utils.isVertical(ev.direction);
26889
26890 // block the browser events
26891 if((inst.options.dragBlockVertical && isVertical) ||
26892 (inst.options.dragBlockHorizontal && !isVertical)) {
26893 ev.preventDefault();
26894 }
26895 break;
26896
26897 case EVENT_RELEASE:
26898 if(triggered && ev.changedLength <= inst.options.dragMaxTouches) {
26899 inst.trigger(name + 'end', ev);
26900 triggered = false;
26901 }
26902 break;
26903
26904 case EVENT_END:
26905 triggered = false;
26906 break;
26907 }
26908 }
26909
26910 Hammer.gestures.Drag = {
26911 name: name,
26912 index: 50,
26913 handler: dragGesture,
26914 defaults: {
26915 /**
26916 * minimal movement that have to be made before the drag event gets triggered
26917 * @property dragMinDistance
26918 * @type {Number}
26919 * @default 10
26920 */
26921 dragMinDistance: 10,
26922
26923 /**
26924 * Set dragDistanceCorrection to true to make the starting point of the drag
26925 * be calculated from where the drag was triggered, not from where the touch started.
26926 * Useful to avoid a jerk-starting drag, which can make fine-adjustments
26927 * through dragging difficult, and be visually unappealing.
26928 * @property dragDistanceCorrection
26929 * @type {Boolean}
26930 * @default true
26931 */
26932 dragDistanceCorrection: true,
26933
26934 /**
26935 * set 0 for unlimited, but this can conflict with transform
26936 * @property dragMaxTouches
26937 * @type {Number}
26938 * @default 1
26939 */
26940 dragMaxTouches: 1,
26941
26942 /**
26943 * prevent default browser behavior when dragging occurs
26944 * be careful with it, it makes the element a blocking element
26945 * when you are using the drag gesture, it is a good practice to set this true
26946 * @property dragBlockHorizontal
26947 * @type {Boolean}
26948 * @default false
26949 */
26950 dragBlockHorizontal: false,
26951
26952 /**
26953 * same as `dragBlockHorizontal`, but for vertical movement
26954 * @property dragBlockVertical
26955 * @type {Boolean}
26956 * @default false
26957 */
26958 dragBlockVertical: false,
26959
26960 /**
26961 * dragLockToAxis keeps the drag gesture on the axis that it started on,
26962 * It disallows vertical directions if the initial direction was horizontal, and vice versa.
26963 * @property dragLockToAxis
26964 * @type {Boolean}
26965 * @default false
26966 */
26967 dragLockToAxis: false,
26968
26969 /**
26970 * drag lock only kicks in when distance > dragLockMinDistance
26971 * This way, locking occurs only when the distance has become large enough to reliably determine the direction
26972 * @property dragLockMinDistance
26973 * @type {Number}
26974 * @default 25
26975 */
26976 dragLockMinDistance: 25
26977 }
26978 };
26979 })('drag');
26980
26981 /**
26982 * @module gestures
26983 */
26984 /**
26985 * trigger a simple gesture event, so you can do anything in your handler.
26986 * only usable if you know what your doing...
26987 *
26988 * @class Gesture
26989 * @static
26990 */
26991 /**
26992 * @event gesture
26993 * @param {Object} ev
26994 */
26995 Hammer.gestures.Gesture = {
26996 name: 'gesture',
26997 index: 1337,
26998 handler: function releaseGesture(ev, inst) {
26999 inst.trigger(this.name, ev);
27000 }
27001 };
27002
27003 /**
27004 * @module gestures
27005 */
27006 /**
27007 * Touch stays at the same place for x time
27008 *
27009 * @class Hold
27010 * @static
27011 */
27012 /**
27013 * @event hold
27014 * @param {Object} ev
27015 */
27016
27017 /**
27018 * @param {String} name
27019 */
27020 (function(name) {
27021 var timer;
27022
27023 function holdGesture(ev, inst) {
27024 var options = inst.options,
27025 current = Detection.current;
27026
27027 switch(ev.eventType) {
27028 case EVENT_START:
27029 clearTimeout(timer);
27030
27031 // set the gesture so we can check in the timeout if it still is
27032 current.name = name;
27033
27034 // set timer and if after the timeout it still is hold,
27035 // we trigger the hold event
27036 timer = setTimeout(function() {
27037 if(current && current.name == name) {
27038 inst.trigger(name, ev);
27039 }
27040 }, options.holdTimeout);
27041 break;
27042
27043 case EVENT_MOVE:
27044 if(ev.distance > options.holdThreshold) {
27045 clearTimeout(timer);
27046 }
27047 break;
27048
27049 case EVENT_RELEASE:
27050 clearTimeout(timer);
27051 break;
27052 }
27053 }
27054
27055 Hammer.gestures.Hold = {
27056 name: name,
27057 index: 10,
27058 defaults: {
27059 /**
27060 * @property holdTimeout
27061 * @type {Number}
27062 * @default 500
27063 */
27064 holdTimeout: 500,
27065
27066 /**
27067 * movement allowed while holding
27068 * @property holdThreshold
27069 * @type {Number}
27070 * @default 2
27071 */
27072 holdThreshold: 2
27073 },
27074 handler: holdGesture
27075 };
27076 })('hold');
27077
27078 /**
27079 * @module gestures
27080 */
27081 /**
27082 * when a touch is being released from the page
27083 *
27084 * @class Release
27085 * @static
27086 */
27087 /**
27088 * @event release
27089 * @param {Object} ev
27090 */
27091 Hammer.gestures.Release = {
27092 name: 'release',
27093 index: Infinity,
27094 handler: function releaseGesture(ev, inst) {
27095 if(ev.eventType == EVENT_RELEASE) {
27096 inst.trigger(this.name, ev);
27097 }
27098 }
27099 };
27100
27101 /**
27102 * @module gestures
27103 */
27104 /**
27105 * triggers swipe events when the end velocity is above the threshold
27106 * for best usage, set `preventDefault` (on the drag gesture) to `true`
27107 * ````
27108 * hammertime.on("dragleft swipeleft", function(ev) {
27109 * console.log(ev);
27110 * ev.gesture.preventDefault();
27111 * });
27112 * ````
27113 *
27114 * @class Swipe
27115 * @static
27116 */
27117 /**
27118 * @event swipe
27119 * @param {Object} ev
27120 */
27121 /**
27122 * @event swipeleft
27123 * @param {Object} ev
27124 */
27125 /**
27126 * @event swiperight
27127 * @param {Object} ev
27128 */
27129 /**
27130 * @event swipeup
27131 * @param {Object} ev
27132 */
27133 /**
27134 * @event swipedown
27135 * @param {Object} ev
27136 */
27137 Hammer.gestures.Swipe = {
27138 name: 'swipe',
27139 index: 40,
27140 defaults: {
27141 /**
27142 * @property swipeMinTouches
27143 * @type {Number}
27144 * @default 1
27145 */
27146 swipeMinTouches: 1,
27147
27148 /**
27149 * @property swipeMaxTouches
27150 * @type {Number}
27151 * @default 1
27152 */
27153 swipeMaxTouches: 1,
27154
27155 /**
27156 * horizontal swipe velocity
27157 * @property swipeVelocityX
27158 * @type {Number}
27159 * @default 0.6
27160 */
27161 swipeVelocityX: 0.6,
27162
27163 /**
27164 * vertical swipe velocity
27165 * @property swipeVelocityY
27166 * @type {Number}
27167 * @default 0.6
27168 */
27169 swipeVelocityY: 0.6
27170 },
27171
27172 handler: function swipeGesture(ev, inst) {
27173 if(ev.eventType == EVENT_RELEASE) {
27174 var touches = ev.touches.length,
27175 options = inst.options;
27176
27177 // max touches
27178 if(touches < options.swipeMinTouches ||
27179 touches > options.swipeMaxTouches) {
27180 return;
27181 }
27182
27183 // when the distance we moved is too small we skip this gesture
27184 // or we can be already in dragging
27185 if(ev.velocityX > options.swipeVelocityX ||
27186 ev.velocityY > options.swipeVelocityY) {
27187 // trigger swipe events
27188 inst.trigger(this.name, ev);
27189 inst.trigger(this.name + ev.direction, ev);
27190 }
27191 }
27192 }
27193 };
27194
27195 /**
27196 * @module gestures
27197 */
27198 /**
27199 * Single tap and a double tap on a place
27200 *
27201 * @class Tap
27202 * @static
27203 */
27204 /**
27205 * @event tap
27206 * @param {Object} ev
27207 */
27208 /**
27209 * @event doubletap
27210 * @param {Object} ev
27211 */
27212
27213 /**
27214 * @param {String} name
27215 */
27216 (function(name) {
27217 var hasMoved = false;
27218
27219 function tapGesture(ev, inst) {
27220 var options = inst.options,
27221 current = Detection.current,
27222 prev = Detection.previous,
27223 sincePrev,
27224 didDoubleTap;
27225
27226 switch(ev.eventType) {
27227 case EVENT_START:
27228 hasMoved = false;
27229 break;
27230
27231 case EVENT_MOVE:
27232 hasMoved = hasMoved || (ev.distance > options.tapMaxDistance);
27233 break;
27234
27235 case EVENT_END:
27236 if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) {
27237 // previous gesture, for the double tap since these are two different gesture detections
27238 sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
27239 didDoubleTap = false;
27240
27241 // check if double tap
27242 if(prev && prev.name == name &&
27243 (sincePrev && sincePrev < options.doubleTapInterval) &&
27244 ev.distance < options.doubleTapDistance) {
27245 inst.trigger('doubletap', ev);
27246 didDoubleTap = true;
27247 }
27248
27249 // do a single tap
27250 if(!didDoubleTap || options.tapAlways) {
27251 current.name = name;
27252 inst.trigger(current.name, ev);
27253 }
27254 }
27255 break;
27256 }
27257 }
27258
27259 Hammer.gestures.Tap = {
27260 name: name,
27261 index: 100,
27262 handler: tapGesture,
27263 defaults: {
27264 /**
27265 * max time of a tap, this is for the slow tappers
27266 * @property tapMaxTime
27267 * @type {Number}
27268 * @default 250
27269 */
27270 tapMaxTime: 250,
27271
27272 /**
27273 * max distance of movement of a tap, this is for the slow tappers
27274 * @property tapMaxDistance
27275 * @type {Number}
27276 * @default 10
27277 */
27278 tapMaxDistance: 10,
27279
27280 /**
27281 * always trigger the `tap` event, even while double-tapping
27282 * @property tapAlways
27283 * @type {Boolean}
27284 * @default true
27285 */
27286 tapAlways: true,
27287
27288 /**
27289 * max distance between two taps
27290 * @property doubleTapDistance
27291 * @type {Number}
27292 * @default 20
27293 */
27294 doubleTapDistance: 20,
27295
27296 /**
27297 * max time between two taps
27298 * @property doubleTapInterval
27299 * @type {Number}
27300 * @default 300
27301 */
27302 doubleTapInterval: 300
27303 }
27304 };
27305 })('tap');
27306
27307 /**
27308 * @module gestures
27309 */
27310 /**
27311 * when a touch is being touched at the page
27312 *
27313 * @class Touch
27314 * @static
27315 */
27316 /**
27317 * @event touch
27318 * @param {Object} ev
27319 */
27320 Hammer.gestures.Touch = {
27321 name: 'touch',
27322 index: -Infinity,
27323 defaults: {
27324 /**
27325 * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page,
27326 * but it improves gestures like transforming and dragging.
27327 * be careful with using this, it can be very annoying for users to be stuck on the page
27328 * @property preventDefault
27329 * @type {Boolean}
27330 * @default false
27331 */
27332 preventDefault: false,
27333
27334 /**
27335 * disable mouse events, so only touch (or pen!) input triggers events
27336 * @property preventMouse
27337 * @type {Boolean}
27338 * @default false
27339 */
27340 preventMouse: false
27341 },
27342 handler: function touchGesture(ev, inst) {
27343 if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) {
27344 ev.stopDetect();
27345 return;
27346 }
27347
27348 if(inst.options.preventDefault) {
27349 ev.preventDefault();
27350 }
27351
27352 if(ev.eventType == EVENT_TOUCH) {
27353 inst.trigger('touch', ev);
27354 }
27355 }
27356 };
27357
27358 /**
27359 * @module gestures
27360 */
27361 /**
27362 * User want to scale or rotate with 2 fingers
27363 * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the
27364 * `preventDefault` option.
27365 *
27366 * @class Transform
27367 * @static
27368 */
27369 /**
27370 * @event transform
27371 * @param {Object} ev
27372 */
27373 /**
27374 * @event transformstart
27375 * @param {Object} ev
27376 */
27377 /**
27378 * @event transformend
27379 * @param {Object} ev
27380 */
27381 /**
27382 * @event pinchin
27383 * @param {Object} ev
27384 */
27385 /**
27386 * @event pinchout
27387 * @param {Object} ev
27388 */
27389 /**
27390 * @event rotate
27391 * @param {Object} ev
27392 */
27393
27394 /**
27395 * @param {String} name
27396 */
27397 (function(name) {
27398 var triggered = false;
27399
27400 function transformGesture(ev, inst) {
27401 switch(ev.eventType) {
27402 case EVENT_START:
27403 triggered = false;
27404 break;
27405
27406 case EVENT_MOVE:
27407 // at least multitouch
27408 if(ev.touches.length < 2) {
27409 return;
27410 }
27411
27412 var scaleThreshold = Math.abs(1 - ev.scale);
27413 var rotationThreshold = Math.abs(ev.rotation);
27414
27415 // when the distance we moved is too small we skip this gesture
27416 // or we can be already in dragging
27417 if(scaleThreshold < inst.options.transformMinScale &&
27418 rotationThreshold < inst.options.transformMinRotation) {
27419 return;
27420 }
27421
27422 // we are transforming!
27423 Detection.current.name = name;
27424
27425 // first time, trigger dragstart event
27426 if(!triggered) {
27427 inst.trigger(name + 'start', ev);
27428 triggered = true;
27429 }
27430
27431 inst.trigger(name, ev); // basic transform event
27432
27433 // trigger rotate event
27434 if(rotationThreshold > inst.options.transformMinRotation) {
27435 inst.trigger('rotate', ev);
27436 }
27437
27438 // trigger pinch event
27439 if(scaleThreshold > inst.options.transformMinScale) {
27440 inst.trigger('pinch', ev);
27441 inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev);
27442 }
27443 break;
27444
27445 case EVENT_RELEASE:
27446 if(triggered && ev.changedLength < 2) {
27447 inst.trigger(name + 'end', ev);
27448 triggered = false;
27449 }
27450 break;
27451 }
27452 }
27453
27454 Hammer.gestures.Transform = {
27455 name: name,
27456 index: 45,
27457 defaults: {
27458 /**
27459 * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
27460 * @property transformMinScale
27461 * @type {Number}
27462 * @default 0.01
27463 */
27464 transformMinScale: 0.01,
27465
27466 /**
27467 * rotation in degrees
27468 * @property transformMinRotation
27469 * @type {Number}
27470 * @default 1
27471 */
27472 transformMinRotation: 1
27473 },
27474
27475 handler: transformGesture
27476 };
27477 })('transform');
27478
27479 /**
27480 * @module hammer
27481 */
27482
27483 // AMD export
27484 if(true) {
27485 !(__WEBPACK_AMD_DEFINE_RESULT__ = function() {
27486 return Hammer;
27487 }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
27488 // commonjs export
27489 } else if(typeof module !== 'undefined' && module.exports) {
27490 module.exports = Hammer;
27491 // browser export
27492 } else {
27493 window.Hammer = Hammer;
27494 }
27495
27496 })(window);
27497
27498/***/ },
27499/* 54 */
27500/***/ function(module, exports, __webpack_require__) {
27501
27502 /**
27503 * Creation of the ClusterMixin var.
27504 *
27505 * This contains all the functions the Network object can use to employ clustering
27506 */
27507
27508 /**
27509 * This is only called in the constructor of the network object
27510 *
27511 */
27512 exports.startWithClustering = function() {
27513 // cluster if the data set is big
27514 this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
27515
27516 // updates the lables after clustering
27517 this.updateLabels();
27518
27519 // this is called here because if clusterin is disabled, the start and stabilize are called in
27520 // the setData function.
27521 if (this.stabilize) {
27522 this._stabilize();
27523 }
27524 this.start();
27525 };
27526
27527 /**
27528 * This function clusters until the initialMaxNodes has been reached
27529 *
27530 * @param {Number} maxNumberOfNodes
27531 * @param {Boolean} reposition
27532 */
27533 exports.clusterToFit = function(maxNumberOfNodes, reposition) {
27534 var numberOfNodes = this.nodeIndices.length;
27535
27536 var maxLevels = 50;
27537 var level = 0;
27538
27539 // we first cluster the hubs, then we pull in the outliers, repeat
27540 while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
27541 if (level % 3 == 0) {
27542 this.forceAggregateHubs(true);
27543 this.normalizeClusterLevels();
27544 }
27545 else {
27546 this.increaseClusterLevel(); // this also includes a cluster normalization
27547 }
27548
27549 numberOfNodes = this.nodeIndices.length;
27550 level += 1;
27551 }
27552
27553 // after the clustering we reposition the nodes to reduce the initial chaos
27554 if (level > 0 && reposition == true) {
27555 this.repositionNodes();
27556 }
27557 this._updateCalculationNodes();
27558 };
27559
27560 /**
27561 * This function can be called to open up a specific cluster. It is only called by
27562 * It will unpack the cluster back one level.
27563 *
27564 * @param node | Node object: cluster to open.
27565 */
27566 exports.openCluster = function(node) {
27567 var isMovingBeforeClustering = this.moving;
27568 if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
27569 !(this._sector() == "default" && this.nodeIndices.length == 1)) {
27570 // this loads a new sector, loads the nodes and edges and nodeIndices of it.
27571 this._addSector(node);
27572 var level = 0;
27573
27574 // we decluster until we reach a decent number of nodes
27575 while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
27576 this.decreaseClusterLevel();
27577 level += 1;
27578 }
27579
27580 }
27581 else {
27582 this._expandClusterNode(node,false,true);
27583
27584 // update the index list, dynamic edges and labels
27585 this._updateNodeIndexList();
27586 this._updateDynamicEdges();
27587 this._updateCalculationNodes();
27588 this.updateLabels();
27589 }
27590
27591 // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
27592 if (this.moving != isMovingBeforeClustering) {
27593 this.start();
27594 }
27595 };
27596
27597
27598 /**
27599 * This calls the updateClustes with default arguments
27600 */
27601 exports.updateClustersDefault = function() {
27602 if (this.constants.clustering.enabled == true) {
27603 this.updateClusters(0,false,false);
27604 }
27605 };
27606
27607
27608 /**
27609 * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
27610 * be clustered with their connected node. This can be repeated as many times as needed.
27611 * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
27612 */
27613 exports.increaseClusterLevel = function() {
27614 this.updateClusters(-1,false,true);
27615 };
27616
27617
27618 /**
27619 * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
27620 * be unpacked if they are a cluster. This can be repeated as many times as needed.
27621 * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
27622 */
27623 exports.decreaseClusterLevel = function() {
27624 this.updateClusters(1,false,true);
27625 };
27626
27627
27628 /**
27629 * This is the main clustering function. It clusters and declusters on zoom or forced
27630 * This function clusters on zoom, it can be called with a predefined zoom direction
27631 * If out, check if we can form clusters, if in, check if we can open clusters.
27632 * This function is only called from _zoom()
27633 *
27634 * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
27635 * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
27636 * @param {Boolean} force | enabled or disable forcing
27637 * @param {Boolean} doNotStart | if true do not call start
27638 *
27639 */
27640 exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
27641 var isMovingBeforeClustering = this.moving;
27642 var amountOfNodes = this.nodeIndices.length;
27643
27644 // on zoom out collapse the sector if the scale is at the level the sector was made
27645 if (this.previousScale > this.scale && zoomDirection == 0) {
27646 this._collapseSector();
27647 }
27648
27649 // check if we zoom in or out
27650 if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
27651 // forming clusters when forced pulls outliers in. When not forced, the edge length of the
27652 // outer nodes determines if it is being clustered
27653 this._formClusters(force);
27654 }
27655 else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
27656 if (force == true) {
27657 // _openClusters checks for each node if the formationScale of the cluster is smaller than
27658 // the current scale and if so, declusters. When forced, all clusters are reduced by one step
27659 this._openClusters(recursive,force);
27660 }
27661 else {
27662 // if a cluster takes up a set percentage of the active window
27663 this._openClustersBySize();
27664 }
27665 }
27666 this._updateNodeIndexList();
27667
27668 // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
27669 if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
27670 this._aggregateHubs(force);
27671 this._updateNodeIndexList();
27672 }
27673
27674 // we now reduce chains.
27675 if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
27676 this.handleChains();
27677 this._updateNodeIndexList();
27678 }
27679
27680 this.previousScale = this.scale;
27681
27682 // rest of the update the index list, dynamic edges and labels
27683 this._updateDynamicEdges();
27684 this.updateLabels();
27685
27686 // if a cluster was formed, we increase the clusterSession
27687 if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
27688 this.clusterSession += 1;
27689 // if clusters have been made, we normalize the cluster level
27690 this.normalizeClusterLevels();
27691 }
27692
27693 if (doNotStart == false || doNotStart === undefined) {
27694 // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
27695 if (this.moving != isMovingBeforeClustering) {
27696 this.start();
27697 }
27698 }
27699
27700 this._updateCalculationNodes();
27701 };
27702
27703 /**
27704 * This function handles the chains. It is called on every updateClusters().
27705 */
27706 exports.handleChains = function() {
27707 // after clustering we check how many chains there are
27708 var chainPercentage = this._getChainFraction();
27709 if (chainPercentage > this.constants.clustering.chainThreshold) {
27710 this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
27711
27712 }
27713 };
27714
27715 /**
27716 * this functions starts clustering by hubs
27717 * The minimum hub threshold is set globally
27718 *
27719 * @private
27720 */
27721 exports._aggregateHubs = function(force) {
27722 this._getHubSize();
27723 this._formClustersByHub(force,false);
27724 };
27725
27726
27727 /**
27728 * This function is fired by keypress. It forces hubs to form.
27729 *
27730 */
27731 exports.forceAggregateHubs = function(doNotStart) {
27732 var isMovingBeforeClustering = this.moving;
27733 var amountOfNodes = this.nodeIndices.length;
27734
27735 this._aggregateHubs(true);
27736
27737 // update the index list, dynamic edges and labels
27738 this._updateNodeIndexList();
27739 this._updateDynamicEdges();
27740 this.updateLabels();
27741
27742 // if a cluster was formed, we increase the clusterSession
27743 if (this.nodeIndices.length != amountOfNodes) {
27744 this.clusterSession += 1;
27745 }
27746
27747 if (doNotStart == false || doNotStart === undefined) {
27748 // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
27749 if (this.moving != isMovingBeforeClustering) {
27750 this.start();
27751 }
27752 }
27753 };
27754
27755 /**
27756 * If a cluster takes up more than a set percentage of the screen, open the cluster
27757 *
27758 * @private
27759 */
27760 exports._openClustersBySize = function() {
27761 for (var nodeId in this.nodes) {
27762 if (this.nodes.hasOwnProperty(nodeId)) {
27763 var node = this.nodes[nodeId];
27764 if (node.inView() == true) {
27765 if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
27766 (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
27767 this.openCluster(node);
27768 }
27769 }
27770 }
27771 }
27772 };
27773
27774
27775 /**
27776 * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
27777 * has to be opened based on the current zoom level.
27778 *
27779 * @private
27780 */
27781 exports._openClusters = function(recursive,force) {
27782 for (var i = 0; i < this.nodeIndices.length; i++) {
27783 var node = this.nodes[this.nodeIndices[i]];
27784 this._expandClusterNode(node,recursive,force);
27785 this._updateCalculationNodes();
27786 }
27787 };
27788
27789 /**
27790 * This function checks if a node has to be opened. This is done by checking the zoom level.
27791 * If the node contains child nodes, this function is recursively called on the child nodes as well.
27792 * This recursive behaviour is optional and can be set by the recursive argument.
27793 *
27794 * @param {Node} parentNode | to check for cluster and expand
27795 * @param {Boolean} recursive | enabled or disable recursive calling
27796 * @param {Boolean} force | enabled or disable forcing
27797 * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
27798 * @private
27799 */
27800 exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
27801 // first check if node is a cluster
27802 if (parentNode.clusterSize > 1) {
27803 // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
27804 if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
27805 openAll = true;
27806 }
27807 recursive = openAll ? true : recursive;
27808
27809 // if the last child has been added on a smaller scale than current scale decluster
27810 if (parentNode.formationScale < this.scale || force == true) {
27811 // we will check if any of the contained child nodes should be removed from the cluster
27812 for (var containedNodeId in parentNode.containedNodes) {
27813 if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
27814 var childNode = parentNode.containedNodes[containedNodeId];
27815
27816 // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
27817 // the largest cluster is the one that comes from outside
27818 if (force == true) {
27819 if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
27820 || openAll) {
27821 this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
27822 }
27823 }
27824 else {
27825 if (this._nodeInActiveArea(parentNode)) {
27826 this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
27827 }
27828 }
27829 }
27830 }
27831 }
27832 }
27833 };
27834
27835 /**
27836 * ONLY CALLED FROM _expandClusterNode
27837 *
27838 * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
27839 * the child node from the parent contained_node object and put it back into the global nodes object.
27840 * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
27841 *
27842 * @param {Node} parentNode | the parent node
27843 * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
27844 * @param {Boolean} recursive | This will also check if the child needs to be expanded.
27845 * With force and recursive both true, the entire cluster is unpacked
27846 * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
27847 * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
27848 * @private
27849 */
27850 exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
27851 var childNode = parentNode.containedNodes[containedNodeId];
27852
27853 // if child node has been added on smaller scale than current, kick out
27854 if (childNode.formationScale < this.scale || force == true) {
27855 // unselect all selected items
27856 this._unselectAll();
27857
27858 // put the child node back in the global nodes object
27859 this.nodes[containedNodeId] = childNode;
27860
27861 // release the contained edges from this childNode back into the global edges
27862 this._releaseContainedEdges(parentNode,childNode);
27863
27864 // reconnect rerouted edges to the childNode
27865 this._connectEdgeBackToChild(parentNode,childNode);
27866
27867 // validate all edges in dynamicEdges
27868 this._validateEdges(parentNode);
27869
27870 // undo the changes from the clustering operation on the parent node
27871 parentNode.options.mass -= childNode.options.mass;
27872 parentNode.clusterSize -= childNode.clusterSize;
27873 parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
27874 parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
27875
27876 // place the child node near the parent, not at the exact same location to avoid chaos in the system
27877 childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
27878 childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
27879
27880 // remove node from the list
27881 delete parentNode.containedNodes[containedNodeId];
27882
27883 // check if there are other childs with this clusterSession in the parent.
27884 var othersPresent = false;
27885 for (var childNodeId in parentNode.containedNodes) {
27886 if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
27887 if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
27888 othersPresent = true;
27889 break;
27890 }
27891 }
27892 }
27893 // if there are no others, remove the cluster session from the list
27894 if (othersPresent == false) {
27895 parentNode.clusterSessions.pop();
27896 }
27897
27898 this._repositionBezierNodes(childNode);
27899 // this._repositionBezierNodes(parentNode);
27900
27901 // remove the clusterSession from the child node
27902 childNode.clusterSession = 0;
27903
27904 // recalculate the size of the node on the next time the node is rendered
27905 parentNode.clearSizeCache();
27906
27907 // restart the simulation to reorganise all nodes
27908 this.moving = true;
27909 }
27910
27911 // check if a further expansion step is possible if recursivity is enabled
27912 if (recursive == true) {
27913 this._expandClusterNode(childNode,recursive,force,openAll);
27914 }
27915 };
27916
27917
27918 /**
27919 * position the bezier nodes at the center of the edges
27920 *
27921 * @param node
27922 * @private
27923 */
27924 exports._repositionBezierNodes = function(node) {
27925 for (var i = 0; i < node.dynamicEdges.length; i++) {
27926 node.dynamicEdges[i].positionBezierNode();
27927 }
27928 };
27929
27930
27931 /**
27932 * This function checks if any nodes at the end of their trees have edges below a threshold length
27933 * This function is called only from updateClusters()
27934 * forceLevelCollapse ignores the length of the edge and collapses one level
27935 * This means that a node with only one edge will be clustered with its connected node
27936 *
27937 * @private
27938 * @param {Boolean} force
27939 */
27940 exports._formClusters = function(force) {
27941 if (force == false) {
27942 this._formClustersByZoom();
27943 }
27944 else {
27945 this._forceClustersByZoom();
27946 }
27947 };
27948
27949
27950 /**
27951 * This function handles the clustering by zooming out, this is based on a minimum edge distance
27952 *
27953 * @private
27954 */
27955 exports._formClustersByZoom = function() {
27956 var dx,dy,length,
27957 minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
27958
27959 // check if any edges are shorter than minLength and start the clustering
27960 // the clustering favours the node with the larger mass
27961 for (var edgeId in this.edges) {
27962 if (this.edges.hasOwnProperty(edgeId)) {
27963 var edge = this.edges[edgeId];
27964 if (edge.connected) {
27965 if (edge.toId != edge.fromId) {
27966 dx = (edge.to.x - edge.from.x);
27967 dy = (edge.to.y - edge.from.y);
27968 length = Math.sqrt(dx * dx + dy * dy);
27969
27970
27971 if (length < minLength) {
27972 // first check which node is larger
27973 var parentNode = edge.from;
27974 var childNode = edge.to;
27975 if (edge.to.options.mass > edge.from.options.mass) {
27976 parentNode = edge.to;
27977 childNode = edge.from;
27978 }
27979
27980 if (childNode.dynamicEdgesLength == 1) {
27981 this._addToCluster(parentNode,childNode,false);
27982 }
27983 else if (parentNode.dynamicEdgesLength == 1) {
27984 this._addToCluster(childNode,parentNode,false);
27985 }
27986 }
27987 }
27988 }
27989 }
27990 }
27991 };
27992
27993 /**
27994 * This function forces the network to cluster all nodes with only one connecting edge to their
27995 * connected node.
27996 *
27997 * @private
27998 */
27999 exports._forceClustersByZoom = function() {
28000 for (var nodeId in this.nodes) {
28001 // another node could have absorbed this child.
28002 if (this.nodes.hasOwnProperty(nodeId)) {
28003 var childNode = this.nodes[nodeId];
28004
28005 // the edges can be swallowed by another decrease
28006 if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
28007 var edge = childNode.dynamicEdges[0];
28008 var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
28009
28010 // group to the largest node
28011 if (childNode.id != parentNode.id) {
28012 if (parentNode.options.mass > childNode.options.mass) {
28013 this._addToCluster(parentNode,childNode,true);
28014 }
28015 else {
28016 this._addToCluster(childNode,parentNode,true);
28017 }
28018 }
28019 }
28020 }
28021 }
28022 };
28023
28024
28025 /**
28026 * To keep the nodes of roughly equal size we normalize the cluster levels.
28027 * This function clusters a node to its smallest connected neighbour.
28028 *
28029 * @param node
28030 * @private
28031 */
28032 exports._clusterToSmallestNeighbour = function(node) {
28033 var smallestNeighbour = -1;
28034 var smallestNeighbourNode = null;
28035 for (var i = 0; i < node.dynamicEdges.length; i++) {
28036 if (node.dynamicEdges[i] !== undefined) {
28037 var neighbour = null;
28038 if (node.dynamicEdges[i].fromId != node.id) {
28039 neighbour = node.dynamicEdges[i].from;
28040 }
28041 else if (node.dynamicEdges[i].toId != node.id) {
28042 neighbour = node.dynamicEdges[i].to;
28043 }
28044
28045
28046 if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
28047 smallestNeighbour = neighbour.clusterSessions.length;
28048 smallestNeighbourNode = neighbour;
28049 }
28050 }
28051 }
28052
28053 if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
28054 this._addToCluster(neighbour, node, true);
28055 }
28056 };
28057
28058
28059 /**
28060 * This function forms clusters from hubs, it loops over all nodes
28061 *
28062 * @param {Boolean} force | Disregard zoom level
28063 * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
28064 * @private
28065 */
28066 exports._formClustersByHub = function(force, onlyEqual) {
28067 // we loop over all nodes in the list
28068 for (var nodeId in this.nodes) {
28069 // we check if it is still available since it can be used by the clustering in this loop
28070 if (this.nodes.hasOwnProperty(nodeId)) {
28071 this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
28072 }
28073 }
28074 };
28075
28076 /**
28077 * This function forms a cluster from a specific preselected hub node
28078 *
28079 * @param {Node} hubNode | the node we will cluster as a hub
28080 * @param {Boolean} force | Disregard zoom level
28081 * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
28082 * @param {Number} [absorptionSizeOffset] |
28083 * @private
28084 */
28085 exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
28086 if (absorptionSizeOffset === undefined) {
28087 absorptionSizeOffset = 0;
28088 }
28089 // we decide if the node is a hub
28090 if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
28091 (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
28092 // initialize variables
28093 var dx,dy,length;
28094 var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
28095 var allowCluster = false;
28096
28097 // we create a list of edges because the dynamicEdges change over the course of this loop
28098 var edgesIdarray = [];
28099 var amountOfInitialEdges = hubNode.dynamicEdges.length;
28100 for (var j = 0; j < amountOfInitialEdges; j++) {
28101 edgesIdarray.push(hubNode.dynamicEdges[j].id);
28102 }
28103
28104 // if the hub clustering is not forces, we check if one of the edges connected
28105 // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
28106 if (force == false) {
28107 allowCluster = false;
28108 for (j = 0; j < amountOfInitialEdges; j++) {
28109 var edge = this.edges[edgesIdarray[j]];
28110 if (edge !== undefined) {
28111 if (edge.connected) {
28112 if (edge.toId != edge.fromId) {
28113 dx = (edge.to.x - edge.from.x);
28114 dy = (edge.to.y - edge.from.y);
28115 length = Math.sqrt(dx * dx + dy * dy);
28116
28117 if (length < minLength) {
28118 allowCluster = true;
28119 break;
28120 }
28121 }
28122 }
28123 }
28124 }
28125 }
28126
28127 // start the clustering if allowed
28128 if ((!force && allowCluster) || force) {
28129 // we loop over all edges INITIALLY connected to this hub
28130 for (j = 0; j < amountOfInitialEdges; j++) {
28131 edge = this.edges[edgesIdarray[j]];
28132 // the edge can be clustered by this function in a previous loop
28133 if (edge !== undefined) {
28134 var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
28135 // we do not want hubs to merge with other hubs nor do we want to cluster itself.
28136 if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
28137 (childNode.id != hubNode.id)) {
28138 this._addToCluster(hubNode,childNode,force);
28139 }
28140 }
28141 }
28142 }
28143 }
28144 };
28145
28146
28147
28148 /**
28149 * This function adds the child node to the parent node, creating a cluster if it is not already.
28150 *
28151 * @param {Node} parentNode | this is the node that will house the child node
28152 * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
28153 * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
28154 * @private
28155 */
28156 exports._addToCluster = function(parentNode, childNode, force) {
28157 // join child node in the parent node
28158 parentNode.containedNodes[childNode.id] = childNode;
28159
28160 // manage all the edges connected to the child and parent nodes
28161 for (var i = 0; i < childNode.dynamicEdges.length; i++) {
28162 var edge = childNode.dynamicEdges[i];
28163 if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
28164 this._addToContainedEdges(parentNode,childNode,edge);
28165 }
28166 else {
28167 this._connectEdgeToCluster(parentNode,childNode,edge);
28168 }
28169 }
28170 // a contained node has no dynamic edges.
28171 childNode.dynamicEdges = [];
28172
28173 // remove circular edges from clusters
28174 this._containCircularEdgesFromNode(parentNode,childNode);
28175
28176
28177 // remove the childNode from the global nodes object
28178 delete this.nodes[childNode.id];
28179
28180 // update the properties of the child and parent
28181 var massBefore = parentNode.options.mass;
28182 childNode.clusterSession = this.clusterSession;
28183 parentNode.options.mass += childNode.options.mass;
28184 parentNode.clusterSize += childNode.clusterSize;
28185 parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
28186
28187 // keep track of the clustersessions so we can open the cluster up as it has been formed.
28188 if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
28189 parentNode.clusterSessions.push(this.clusterSession);
28190 }
28191
28192 // forced clusters only open from screen size and double tap
28193 if (force == true) {
28194 // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
28195 parentNode.formationScale = 0;
28196 }
28197 else {
28198 parentNode.formationScale = this.scale; // The latest child has been added on this scale
28199 }
28200
28201 // recalculate the size of the node on the next time the node is rendered
28202 parentNode.clearSizeCache();
28203
28204 // set the pop-out scale for the childnode
28205 parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
28206
28207 // nullify the movement velocity of the child, this is to avoid hectic behaviour
28208 childNode.clearVelocity();
28209
28210 // the mass has altered, preservation of energy dictates the velocity to be updated
28211 parentNode.updateVelocity(massBefore);
28212
28213 // restart the simulation to reorganise all nodes
28214 this.moving = true;
28215 };
28216
28217
28218 /**
28219 * This function will apply the changes made to the remainingEdges during the formation of the clusters.
28220 * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
28221 * It has to be called if a level is collapsed. It is called by _formClusters().
28222 * @private
28223 */
28224 exports._updateDynamicEdges = function() {
28225 for (var i = 0; i < this.nodeIndices.length; i++) {
28226 var node = this.nodes[this.nodeIndices[i]];
28227 node.dynamicEdgesLength = node.dynamicEdges.length;
28228
28229 // this corrects for multiple edges pointing at the same other node
28230 var correction = 0;
28231 if (node.dynamicEdgesLength > 1) {
28232 for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
28233 var edgeToId = node.dynamicEdges[j].toId;
28234 var edgeFromId = node.dynamicEdges[j].fromId;
28235 for (var k = j+1; k < node.dynamicEdgesLength; k++) {
28236 if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
28237 (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
28238 correction += 1;
28239 }
28240 }
28241 }
28242 }
28243 node.dynamicEdgesLength -= correction;
28244 }
28245 };
28246
28247
28248 /**
28249 * This adds an edge from the childNode to the contained edges of the parent node
28250 *
28251 * @param parentNode | Node object
28252 * @param childNode | Node object
28253 * @param edge | Edge object
28254 * @private
28255 */
28256 exports._addToContainedEdges = function(parentNode, childNode, edge) {
28257 // create an array object if it does not yet exist for this childNode
28258 if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
28259 parentNode.containedEdges[childNode.id] = []
28260 }
28261 // add this edge to the list
28262 parentNode.containedEdges[childNode.id].push(edge);
28263
28264 // remove the edge from the global edges object
28265 delete this.edges[edge.id];
28266
28267 // remove the edge from the parent object
28268 for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
28269 if (parentNode.dynamicEdges[i].id == edge.id) {
28270 parentNode.dynamicEdges.splice(i,1);
28271 break;
28272 }
28273 }
28274 };
28275
28276 /**
28277 * This function connects an edge that was connected to a child node to the parent node.
28278 * It keeps track of which nodes it has been connected to with the originalId array.
28279 *
28280 * @param {Node} parentNode | Node object
28281 * @param {Node} childNode | Node object
28282 * @param {Edge} edge | Edge object
28283 * @private
28284 */
28285 exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
28286 // handle circular edges
28287 if (edge.toId == edge.fromId) {
28288 this._addToContainedEdges(parentNode, childNode, edge);
28289 }
28290 else {
28291 if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
28292 edge.originalToId.push(childNode.id);
28293 edge.to = parentNode;
28294 edge.toId = parentNode.id;
28295 }
28296 else { // edge connected to other node with the "from" side
28297
28298 edge.originalFromId.push(childNode.id);
28299 edge.from = parentNode;
28300 edge.fromId = parentNode.id;
28301 }
28302
28303 this._addToReroutedEdges(parentNode,childNode,edge);
28304 }
28305 };
28306
28307
28308 /**
28309 * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
28310 * these edges inside of the cluster.
28311 *
28312 * @param parentNode
28313 * @param childNode
28314 * @private
28315 */
28316 exports._containCircularEdgesFromNode = function(parentNode, childNode) {
28317 // manage all the edges connected to the child and parent nodes
28318 for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
28319 var edge = parentNode.dynamicEdges[i];
28320 // handle circular edges
28321 if (edge.toId == edge.fromId) {
28322 this._addToContainedEdges(parentNode, childNode, edge);
28323 }
28324 }
28325 };
28326
28327
28328 /**
28329 * This adds an edge from the childNode to the rerouted edges of the parent node
28330 *
28331 * @param parentNode | Node object
28332 * @param childNode | Node object
28333 * @param edge | Edge object
28334 * @private
28335 */
28336 exports._addToReroutedEdges = function(parentNode, childNode, edge) {
28337 // create an array object if it does not yet exist for this childNode
28338 // we store the edge in the rerouted edges so we can restore it when the cluster pops open
28339 if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
28340 parentNode.reroutedEdges[childNode.id] = [];
28341 }
28342 parentNode.reroutedEdges[childNode.id].push(edge);
28343
28344 // this edge becomes part of the dynamicEdges of the cluster node
28345 parentNode.dynamicEdges.push(edge);
28346 };
28347
28348
28349
28350 /**
28351 * This function connects an edge that was connected to a cluster node back to the child node.
28352 *
28353 * @param parentNode | Node object
28354 * @param childNode | Node object
28355 * @private
28356 */
28357 exports._connectEdgeBackToChild = function(parentNode, childNode) {
28358 if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
28359 for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
28360 var edge = parentNode.reroutedEdges[childNode.id][i];
28361 if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
28362 edge.originalFromId.pop();
28363 edge.fromId = childNode.id;
28364 edge.from = childNode;
28365 }
28366 else {
28367 edge.originalToId.pop();
28368 edge.toId = childNode.id;
28369 edge.to = childNode;
28370 }
28371
28372 // append this edge to the list of edges connecting to the childnode
28373 childNode.dynamicEdges.push(edge);
28374
28375 // remove the edge from the parent object
28376 for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
28377 if (parentNode.dynamicEdges[j].id == edge.id) {
28378 parentNode.dynamicEdges.splice(j,1);
28379 break;
28380 }
28381 }
28382 }
28383 // remove the entry from the rerouted edges
28384 delete parentNode.reroutedEdges[childNode.id];
28385 }
28386 };
28387
28388
28389 /**
28390 * When loops are clustered, an edge can be both in the rerouted array and the contained array.
28391 * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
28392 * parentNode
28393 *
28394 * @param parentNode | Node object
28395 * @private
28396 */
28397 exports._validateEdges = function(parentNode) {
28398 for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
28399 var edge = parentNode.dynamicEdges[i];
28400 if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
28401 parentNode.dynamicEdges.splice(i,1);
28402 }
28403 }
28404 };
28405
28406
28407 /**
28408 * This function released the contained edges back into the global domain and puts them back into the
28409 * dynamic edges of both parent and child.
28410 *
28411 * @param {Node} parentNode |
28412 * @param {Node} childNode |
28413 * @private
28414 */
28415 exports._releaseContainedEdges = function(parentNode, childNode) {
28416 for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
28417 var edge = parentNode.containedEdges[childNode.id][i];
28418
28419 // put the edge back in the global edges object
28420 this.edges[edge.id] = edge;
28421
28422 // put the edge back in the dynamic edges of the child and parent
28423 childNode.dynamicEdges.push(edge);
28424 parentNode.dynamicEdges.push(edge);
28425 }
28426 // remove the entry from the contained edges
28427 delete parentNode.containedEdges[childNode.id];
28428
28429 };
28430
28431
28432
28433
28434 // ------------------- UTILITY FUNCTIONS ---------------------------- //
28435
28436
28437 /**
28438 * This updates the node labels for all nodes (for debugging purposes)
28439 */
28440 exports.updateLabels = function() {
28441 var nodeId;
28442 // update node labels
28443 for (nodeId in this.nodes) {
28444 if (this.nodes.hasOwnProperty(nodeId)) {
28445 var node = this.nodes[nodeId];
28446 if (node.clusterSize > 1) {
28447 node.label = "[".concat(String(node.clusterSize),"]");
28448 }
28449 }
28450 }
28451
28452 // update node labels
28453 for (nodeId in this.nodes) {
28454 if (this.nodes.hasOwnProperty(nodeId)) {
28455 node = this.nodes[nodeId];
28456 if (node.clusterSize == 1) {
28457 if (node.originalLabel !== undefined) {
28458 node.label = node.originalLabel;
28459 }
28460 else {
28461 node.label = String(node.id);
28462 }
28463 }
28464 }
28465 }
28466
28467 // /* Debug Override */
28468 // for (nodeId in this.nodes) {
28469 // if (this.nodes.hasOwnProperty(nodeId)) {
28470 // node = this.nodes[nodeId];
28471 // node.label = String(node.level);
28472 // }
28473 // }
28474
28475 };
28476
28477
28478 /**
28479 * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
28480 * if the rest of the nodes are already a few cluster levels in.
28481 * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
28482 * clustered enough to the clusterToSmallestNeighbours function.
28483 */
28484 exports.normalizeClusterLevels = function() {
28485 var maxLevel = 0;
28486 var minLevel = 1e9;
28487 var clusterLevel = 0;
28488 var nodeId;
28489
28490 // we loop over all nodes in the list
28491 for (nodeId in this.nodes) {
28492 if (this.nodes.hasOwnProperty(nodeId)) {
28493 clusterLevel = this.nodes[nodeId].clusterSessions.length;
28494 if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
28495 if (minLevel > clusterLevel) {minLevel = clusterLevel;}
28496 }
28497 }
28498
28499 if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
28500 var amountOfNodes = this.nodeIndices.length;
28501 var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
28502 // we loop over all nodes in the list
28503 for (nodeId in this.nodes) {
28504 if (this.nodes.hasOwnProperty(nodeId)) {
28505 if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
28506 this._clusterToSmallestNeighbour(this.nodes[nodeId]);
28507 }
28508 }
28509 }
28510 this._updateNodeIndexList();
28511 this._updateDynamicEdges();
28512 // if a cluster was formed, we increase the clusterSession
28513 if (this.nodeIndices.length != amountOfNodes) {
28514 this.clusterSession += 1;
28515 }
28516 }
28517 };
28518
28519
28520
28521 /**
28522 * This function determines if the cluster we want to decluster is in the active area
28523 * this means around the zoom center
28524 *
28525 * @param {Node} node
28526 * @returns {boolean}
28527 * @private
28528 */
28529 exports._nodeInActiveArea = function(node) {
28530 return (
28531 Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
28532 &&
28533 Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
28534 )
28535 };
28536
28537
28538 /**
28539 * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
28540 * It puts large clusters away from the center and randomizes the order.
28541 *
28542 */
28543 exports.repositionNodes = function() {
28544 for (var i = 0; i < this.nodeIndices.length; i++) {
28545 var node = this.nodes[this.nodeIndices[i]];
28546 if ((node.xFixed == false || node.yFixed == false)) {
28547 var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.options.mass);
28548 var angle = 2 * Math.PI * Math.random();
28549 if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
28550 if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
28551 this._repositionBezierNodes(node);
28552 }
28553 }
28554 };
28555
28556
28557 /**
28558 * We determine how many connections denote an important hub.
28559 * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
28560 *
28561 * @private
28562 */
28563 exports._getHubSize = function() {
28564 var average = 0;
28565 var averageSquared = 0;
28566 var hubCounter = 0;
28567 var largestHub = 0;
28568
28569 for (var i = 0; i < this.nodeIndices.length; i++) {
28570
28571 var node = this.nodes[this.nodeIndices[i]];
28572 if (node.dynamicEdgesLength > largestHub) {
28573 largestHub = node.dynamicEdgesLength;
28574 }
28575 average += node.dynamicEdgesLength;
28576 averageSquared += Math.pow(node.dynamicEdgesLength,2);
28577 hubCounter += 1;
28578 }
28579 average = average / hubCounter;
28580 averageSquared = averageSquared / hubCounter;
28581
28582 var variance = averageSquared - Math.pow(average,2);
28583
28584 var standardDeviation = Math.sqrt(variance);
28585
28586 this.hubThreshold = Math.floor(average + 2*standardDeviation);
28587
28588 // always have at least one to cluster
28589 if (this.hubThreshold > largestHub) {
28590 this.hubThreshold = largestHub;
28591 }
28592
28593 // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
28594 // console.log("hubThreshold:",this.hubThreshold);
28595 };
28596
28597
28598 /**
28599 * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
28600 * with this amount we can cluster specifically on these chains.
28601 *
28602 * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
28603 * @private
28604 */
28605 exports._reduceAmountOfChains = function(fraction) {
28606 this.hubThreshold = 2;
28607 var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
28608 for (var nodeId in this.nodes) {
28609 if (this.nodes.hasOwnProperty(nodeId)) {
28610 if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
28611 if (reduceAmount > 0) {
28612 this._formClusterFromHub(this.nodes[nodeId],true,true,1);
28613 reduceAmount -= 1;
28614 }
28615 }
28616 }
28617 }
28618 };
28619
28620 /**
28621 * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
28622 * with this amount we can cluster specifically on these chains.
28623 *
28624 * @private
28625 */
28626 exports._getChainFraction = function() {
28627 var chains = 0;
28628 var total = 0;
28629 for (var nodeId in this.nodes) {
28630 if (this.nodes.hasOwnProperty(nodeId)) {
28631 if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
28632 chains += 1;
28633 }
28634 total += 1;
28635 }
28636 }
28637 return chains/total;
28638 };
28639
28640
28641/***/ },
28642/* 55 */
28643/***/ function(module, exports, __webpack_require__) {
28644
28645 var util = __webpack_require__(1);
28646
28647 /**
28648 * Creation of the SectorMixin var.
28649 *
28650 * This contains all the functions the Network object can use to employ the sector system.
28651 * The sector system is always used by Network, though the benefits only apply to the use of clustering.
28652 * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
28653 */
28654
28655 /**
28656 * This function is only called by the setData function of the Network object.
28657 * This loads the global references into the active sector. This initializes the sector.
28658 *
28659 * @private
28660 */
28661 exports._putDataInSector = function() {
28662 this.sectors["active"][this._sector()].nodes = this.nodes;
28663 this.sectors["active"][this._sector()].edges = this.edges;
28664 this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
28665 };
28666
28667
28668 /**
28669 * /**
28670 * This function sets the global references to nodes, edges and nodeIndices back to
28671 * those of the supplied (active) sector. If a type is defined, do the specific type
28672 *
28673 * @param {String} sectorId
28674 * @param {String} [sectorType] | "active" or "frozen"
28675 * @private
28676 */
28677 exports._switchToSector = function(sectorId, sectorType) {
28678 if (sectorType === undefined || sectorType == "active") {
28679 this._switchToActiveSector(sectorId);
28680 }
28681 else {
28682 this._switchToFrozenSector(sectorId);
28683 }
28684 };
28685
28686
28687 /**
28688 * This function sets the global references to nodes, edges and nodeIndices back to
28689 * those of the supplied active sector.
28690 *
28691 * @param sectorId
28692 * @private
28693 */
28694 exports._switchToActiveSector = function(sectorId) {
28695 this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
28696 this.nodes = this.sectors["active"][sectorId]["nodes"];
28697 this.edges = this.sectors["active"][sectorId]["edges"];
28698 };
28699
28700
28701 /**
28702 * This function sets the global references to nodes, edges and nodeIndices back to
28703 * those of the supplied active sector.
28704 *
28705 * @private
28706 */
28707 exports._switchToSupportSector = function() {
28708 this.nodeIndices = this.sectors["support"]["nodeIndices"];
28709 this.nodes = this.sectors["support"]["nodes"];
28710 this.edges = this.sectors["support"]["edges"];
28711 };
28712
28713
28714 /**
28715 * This function sets the global references to nodes, edges and nodeIndices back to
28716 * those of the supplied frozen sector.
28717 *
28718 * @param sectorId
28719 * @private
28720 */
28721 exports._switchToFrozenSector = function(sectorId) {
28722 this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
28723 this.nodes = this.sectors["frozen"][sectorId]["nodes"];
28724 this.edges = this.sectors["frozen"][sectorId]["edges"];
28725 };
28726
28727
28728 /**
28729 * This function sets the global references to nodes, edges and nodeIndices back to
28730 * those of the currently active sector.
28731 *
28732 * @private
28733 */
28734 exports._loadLatestSector = function() {
28735 this._switchToSector(this._sector());
28736 };
28737
28738
28739 /**
28740 * This function returns the currently active sector Id
28741 *
28742 * @returns {String}
28743 * @private
28744 */
28745 exports._sector = function() {
28746 return this.activeSector[this.activeSector.length-1];
28747 };
28748
28749
28750 /**
28751 * This function returns the previously active sector Id
28752 *
28753 * @returns {String}
28754 * @private
28755 */
28756 exports._previousSector = function() {
28757 if (this.activeSector.length > 1) {
28758 return this.activeSector[this.activeSector.length-2];
28759 }
28760 else {
28761 throw new TypeError('there are not enough sectors in the this.activeSector array.');
28762 }
28763 };
28764
28765
28766 /**
28767 * We add the active sector at the end of the this.activeSector array
28768 * This ensures it is the currently active sector returned by _sector() and it reaches the top
28769 * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
28770 *
28771 * @param newId
28772 * @private
28773 */
28774 exports._setActiveSector = function(newId) {
28775 this.activeSector.push(newId);
28776 };
28777
28778
28779 /**
28780 * We remove the currently active sector id from the active sector stack. This happens when
28781 * we reactivate the previously active sector
28782 *
28783 * @private
28784 */
28785 exports._forgetLastSector = function() {
28786 this.activeSector.pop();
28787 };
28788
28789
28790 /**
28791 * This function creates a new active sector with the supplied newId. This newId
28792 * is the expanding node id.
28793 *
28794 * @param {String} newId | Id of the new active sector
28795 * @private
28796 */
28797 exports._createNewSector = function(newId) {
28798 // create the new sector
28799 this.sectors["active"][newId] = {"nodes":{},
28800 "edges":{},
28801 "nodeIndices":[],
28802 "formationScale": this.scale,
28803 "drawingNode": undefined};
28804
28805 // create the new sector render node. This gives visual feedback that you are in a new sector.
28806 this.sectors["active"][newId]['drawingNode'] = new Node(
28807 {id:newId,
28808 color: {
28809 background: "#eaefef",
28810 border: "495c5e"
28811 }
28812 },{},{},this.constants);
28813 this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
28814 };
28815
28816
28817 /**
28818 * This function removes the currently active sector. This is called when we create a new
28819 * active sector.
28820 *
28821 * @param {String} sectorId | Id of the active sector that will be removed
28822 * @private
28823 */
28824 exports._deleteActiveSector = function(sectorId) {
28825 delete this.sectors["active"][sectorId];
28826 };
28827
28828
28829 /**
28830 * This function removes the currently active sector. This is called when we reactivate
28831 * the previously active sector.
28832 *
28833 * @param {String} sectorId | Id of the active sector that will be removed
28834 * @private
28835 */
28836 exports._deleteFrozenSector = function(sectorId) {
28837 delete this.sectors["frozen"][sectorId];
28838 };
28839
28840
28841 /**
28842 * Freezing an active sector means moving it from the "active" object to the "frozen" object.
28843 * We copy the references, then delete the active entree.
28844 *
28845 * @param sectorId
28846 * @private
28847 */
28848 exports._freezeSector = function(sectorId) {
28849 // we move the set references from the active to the frozen stack.
28850 this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
28851
28852 // we have moved the sector data into the frozen set, we now remove it from the active set
28853 this._deleteActiveSector(sectorId);
28854 };
28855
28856
28857 /**
28858 * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
28859 * object to the "active" object.
28860 *
28861 * @param sectorId
28862 * @private
28863 */
28864 exports._activateSector = function(sectorId) {
28865 // we move the set references from the frozen to the active stack.
28866 this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
28867
28868 // we have moved the sector data into the active set, we now remove it from the frozen stack
28869 this._deleteFrozenSector(sectorId);
28870 };
28871
28872
28873 /**
28874 * This function merges the data from the currently active sector with a frozen sector. This is used
28875 * in the process of reverting back to the previously active sector.
28876 * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
28877 * upon the creation of a new active sector.
28878 *
28879 * @param sectorId
28880 * @private
28881 */
28882 exports._mergeThisWithFrozen = function(sectorId) {
28883 // copy all nodes
28884 for (var nodeId in this.nodes) {
28885 if (this.nodes.hasOwnProperty(nodeId)) {
28886 this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
28887 }
28888 }
28889
28890 // copy all edges (if not fully clustered, else there are no edges)
28891 for (var edgeId in this.edges) {
28892 if (this.edges.hasOwnProperty(edgeId)) {
28893 this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
28894 }
28895 }
28896
28897 // merge the nodeIndices
28898 for (var i = 0; i < this.nodeIndices.length; i++) {
28899 this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
28900 }
28901 };
28902
28903
28904 /**
28905 * This clusters the sector to one cluster. It was a single cluster before this process started so
28906 * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
28907 *
28908 * @private
28909 */
28910 exports._collapseThisToSingleCluster = function() {
28911 this.clusterToFit(1,false);
28912 };
28913
28914
28915 /**
28916 * We create a new active sector from the node that we want to open.
28917 *
28918 * @param node
28919 * @private
28920 */
28921 exports._addSector = function(node) {
28922 // this is the currently active sector
28923 var sector = this._sector();
28924
28925 // // this should allow me to select nodes from a frozen set.
28926 // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
28927 // console.log("the node is part of the active sector");
28928 // }
28929 // else {
28930 // console.log("I dont know what the fuck happened!!");
28931 // }
28932
28933 // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
28934 delete this.nodes[node.id];
28935
28936 var unqiueIdentifier = util.randomUUID();
28937
28938 // we fully freeze the currently active sector
28939 this._freezeSector(sector);
28940
28941 // we create a new active sector. This sector has the Id of the node to ensure uniqueness
28942 this._createNewSector(unqiueIdentifier);
28943
28944 // we add the active sector to the sectors array to be able to revert these steps later on
28945 this._setActiveSector(unqiueIdentifier);
28946
28947 // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
28948 this._switchToSector(this._sector());
28949
28950 // finally we add the node we removed from our previous active sector to the new active sector
28951 this.nodes[node.id] = node;
28952 };
28953
28954
28955 /**
28956 * We close the sector that is currently open and revert back to the one before.
28957 * If the active sector is the "default" sector, nothing happens.
28958 *
28959 * @private
28960 */
28961 exports._collapseSector = function() {
28962 // the currently active sector
28963 var sector = this._sector();
28964
28965 // we cannot collapse the default sector
28966 if (sector != "default") {
28967 if ((this.nodeIndices.length == 1) ||
28968 (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
28969 (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
28970 var previousSector = this._previousSector();
28971
28972 // we collapse the sector back to a single cluster
28973 this._collapseThisToSingleCluster();
28974
28975 // we move the remaining nodes, edges and nodeIndices to the previous sector.
28976 // This previous sector is the one we will reactivate
28977 this._mergeThisWithFrozen(previousSector);
28978
28979 // the previously active (frozen) sector now has all the data from the currently active sector.
28980 // we can now delete the active sector.
28981 this._deleteActiveSector(sector);
28982
28983 // we activate the previously active (and currently frozen) sector.
28984 this._activateSector(previousSector);
28985
28986 // we load the references from the newly active sector into the global references
28987 this._switchToSector(previousSector);
28988
28989 // we forget the previously active sector because we reverted to the one before
28990 this._forgetLastSector();
28991
28992 // finally, we update the node index list.
28993 this._updateNodeIndexList();
28994
28995 // we refresh the list with calulation nodes and calculation node indices.
28996 this._updateCalculationNodes();
28997 }
28998 }
28999 };
29000
29001
29002 /**
29003 * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
29004 *
29005 * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
29006 * | we dont pass the function itself because then the "this" is the window object
29007 * | instead of the Network object
29008 * @param {*} [argument] | Optional: arguments to pass to the runFunction
29009 * @private
29010 */
29011 exports._doInAllActiveSectors = function(runFunction,argument) {
29012 var returnValues = [];
29013 if (argument === undefined) {
29014 for (var sector in this.sectors["active"]) {
29015 if (this.sectors["active"].hasOwnProperty(sector)) {
29016 // switch the global references to those of this sector
29017 this._switchToActiveSector(sector);
29018 returnValues.push( this[runFunction]() );
29019 }
29020 }
29021 }
29022 else {
29023 for (var sector in this.sectors["active"]) {
29024 if (this.sectors["active"].hasOwnProperty(sector)) {
29025 // switch the global references to those of this sector
29026 this._switchToActiveSector(sector);
29027 var args = Array.prototype.splice.call(arguments, 1);
29028 if (args.length > 1) {
29029 returnValues.push( this[runFunction](args[0],args[1]) );
29030 }
29031 else {
29032 returnValues.push( this[runFunction](argument) );
29033 }
29034 }
29035 }
29036 }
29037 // we revert the global references back to our active sector
29038 this._loadLatestSector();
29039 return returnValues;
29040 };
29041
29042
29043 /**
29044 * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
29045 *
29046 * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
29047 * | we dont pass the function itself because then the "this" is the window object
29048 * | instead of the Network object
29049 * @param {*} [argument] | Optional: arguments to pass to the runFunction
29050 * @private
29051 */
29052 exports._doInSupportSector = function(runFunction,argument) {
29053 var returnValues = false;
29054 if (argument === undefined) {
29055 this._switchToSupportSector();
29056 returnValues = this[runFunction]();
29057 }
29058 else {
29059 this._switchToSupportSector();
29060 var args = Array.prototype.splice.call(arguments, 1);
29061 if (args.length > 1) {
29062 returnValues = this[runFunction](args[0],args[1]);
29063 }
29064 else {
29065 returnValues = this[runFunction](argument);
29066 }
29067 }
29068 // we revert the global references back to our active sector
29069 this._loadLatestSector();
29070 return returnValues;
29071 };
29072
29073
29074 /**
29075 * This runs a function in all frozen sectors. This is used in the _redraw().
29076 *
29077 * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
29078 * | we don't pass the function itself because then the "this" is the window object
29079 * | instead of the Network object
29080 * @param {*} [argument] | Optional: arguments to pass to the runFunction
29081 * @private
29082 */
29083 exports._doInAllFrozenSectors = function(runFunction,argument) {
29084 if (argument === undefined) {
29085 for (var sector in this.sectors["frozen"]) {
29086 if (this.sectors["frozen"].hasOwnProperty(sector)) {
29087 // switch the global references to those of this sector
29088 this._switchToFrozenSector(sector);
29089 this[runFunction]();
29090 }
29091 }
29092 }
29093 else {
29094 for (var sector in this.sectors["frozen"]) {
29095 if (this.sectors["frozen"].hasOwnProperty(sector)) {
29096 // switch the global references to those of this sector
29097 this._switchToFrozenSector(sector);
29098 var args = Array.prototype.splice.call(arguments, 1);
29099 if (args.length > 1) {
29100 this[runFunction](args[0],args[1]);
29101 }
29102 else {
29103 this[runFunction](argument);
29104 }
29105 }
29106 }
29107 }
29108 this._loadLatestSector();
29109 };
29110
29111
29112 /**
29113 * This runs a function in all sectors. This is used in the _redraw().
29114 *
29115 * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
29116 * | we don't pass the function itself because then the "this" is the window object
29117 * | instead of the Network object
29118 * @param {*} [argument] | Optional: arguments to pass to the runFunction
29119 * @private
29120 */
29121 exports._doInAllSectors = function(runFunction,argument) {
29122 var args = Array.prototype.splice.call(arguments, 1);
29123 if (argument === undefined) {
29124 this._doInAllActiveSectors(runFunction);
29125 this._doInAllFrozenSectors(runFunction);
29126 }
29127 else {
29128 if (args.length > 1) {
29129 this._doInAllActiveSectors(runFunction,args[0],args[1]);
29130 this._doInAllFrozenSectors(runFunction,args[0],args[1]);
29131 }
29132 else {
29133 this._doInAllActiveSectors(runFunction,argument);
29134 this._doInAllFrozenSectors(runFunction,argument);
29135 }
29136 }
29137 };
29138
29139
29140 /**
29141 * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
29142 * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
29143 *
29144 * @private
29145 */
29146 exports._clearNodeIndexList = function() {
29147 var sector = this._sector();
29148 this.sectors["active"][sector]["nodeIndices"] = [];
29149 this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
29150 };
29151
29152
29153 /**
29154 * Draw the encompassing sector node
29155 *
29156 * @param ctx
29157 * @param sectorType
29158 * @private
29159 */
29160 exports._drawSectorNodes = function(ctx,sectorType) {
29161 var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
29162 for (var sector in this.sectors[sectorType]) {
29163 if (this.sectors[sectorType].hasOwnProperty(sector)) {
29164 if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
29165
29166 this._switchToSector(sector,sectorType);
29167
29168 minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
29169 for (var nodeId in this.nodes) {
29170 if (this.nodes.hasOwnProperty(nodeId)) {
29171 node = this.nodes[nodeId];
29172 node.resize(ctx);
29173 if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
29174 if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
29175 if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
29176 if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
29177 }
29178 }
29179 node = this.sectors[sectorType][sector]["drawingNode"];
29180 node.x = 0.5 * (maxX + minX);
29181 node.y = 0.5 * (maxY + minY);
29182 node.width = 2 * (node.x - minX);
29183 node.height = 2 * (node.y - minY);
29184 node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
29185 node.setScale(this.scale);
29186 node._drawCircle(ctx);
29187 }
29188 }
29189 }
29190 };
29191
29192 exports._drawAllSectorNodes = function(ctx) {
29193 this._drawSectorNodes(ctx,"frozen");
29194 this._drawSectorNodes(ctx,"active");
29195 this._loadLatestSector();
29196 };
29197
29198
29199/***/ },
29200/* 56 */
29201/***/ function(module, exports, __webpack_require__) {
29202
29203 var Node = __webpack_require__(37);
29204
29205 /**
29206 * This function can be called from the _doInAllSectors function
29207 *
29208 * @param object
29209 * @param overlappingNodes
29210 * @private
29211 */
29212 exports._getNodesOverlappingWith = function(object, overlappingNodes) {
29213 var nodes = this.nodes;
29214 for (var nodeId in nodes) {
29215 if (nodes.hasOwnProperty(nodeId)) {
29216 if (nodes[nodeId].isOverlappingWith(object)) {
29217 overlappingNodes.push(nodeId);
29218 }
29219 }
29220 }
29221 };
29222
29223 /**
29224 * retrieve all nodes overlapping with given object
29225 * @param {Object} object An object with parameters left, top, right, bottom
29226 * @return {Number[]} An array with id's of the overlapping nodes
29227 * @private
29228 */
29229 exports._getAllNodesOverlappingWith = function (object) {
29230 var overlappingNodes = [];
29231 this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
29232 return overlappingNodes;
29233 };
29234
29235
29236 /**
29237 * Return a position object in canvasspace from a single point in screenspace
29238 *
29239 * @param pointer
29240 * @returns {{left: number, top: number, right: number, bottom: number}}
29241 * @private
29242 */
29243 exports._pointerToPositionObject = function(pointer) {
29244 var x = this._XconvertDOMtoCanvas(pointer.x);
29245 var y = this._YconvertDOMtoCanvas(pointer.y);
29246
29247 return {
29248 left: x,
29249 top: y,
29250 right: x,
29251 bottom: y
29252 };
29253 };
29254
29255
29256 /**
29257 * Get the top node at the a specific point (like a click)
29258 *
29259 * @param {{x: Number, y: Number}} pointer
29260 * @return {Node | null} node
29261 * @private
29262 */
29263 exports._getNodeAt = function (pointer) {
29264 // we first check if this is an navigation controls element
29265 var positionObject = this._pointerToPositionObject(pointer);
29266 var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
29267
29268 // if there are overlapping nodes, select the last one, this is the
29269 // one which is drawn on top of the others
29270 if (overlappingNodes.length > 0) {
29271 return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
29272 }
29273 else {
29274 return null;
29275 }
29276 };
29277
29278
29279 /**
29280 * retrieve all edges overlapping with given object, selector is around center
29281 * @param {Object} object An object with parameters left, top, right, bottom
29282 * @return {Number[]} An array with id's of the overlapping nodes
29283 * @private
29284 */
29285 exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
29286 var edges = this.edges;
29287 for (var edgeId in edges) {
29288 if (edges.hasOwnProperty(edgeId)) {
29289 if (edges[edgeId].isOverlappingWith(object)) {
29290 overlappingEdges.push(edgeId);
29291 }
29292 }
29293 }
29294 };
29295
29296
29297 /**
29298 * retrieve all nodes overlapping with given object
29299 * @param {Object} object An object with parameters left, top, right, bottom
29300 * @return {Number[]} An array with id's of the overlapping nodes
29301 * @private
29302 */
29303 exports._getAllEdgesOverlappingWith = function (object) {
29304 var overlappingEdges = [];
29305 this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
29306 return overlappingEdges;
29307 };
29308
29309 /**
29310 * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
29311 * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
29312 *
29313 * @param pointer
29314 * @returns {null}
29315 * @private
29316 */
29317 exports._getEdgeAt = function(pointer) {
29318 var positionObject = this._pointerToPositionObject(pointer);
29319 var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
29320
29321 if (overlappingEdges.length > 0) {
29322 return this.edges[overlappingEdges[overlappingEdges.length - 1]];
29323 }
29324 else {
29325 return null;
29326 }
29327 };
29328
29329
29330 /**
29331 * Add object to the selection array.
29332 *
29333 * @param obj
29334 * @private
29335 */
29336 exports._addToSelection = function(obj) {
29337 if (obj instanceof Node) {
29338 this.selectionObj.nodes[obj.id] = obj;
29339 }
29340 else {
29341 this.selectionObj.edges[obj.id] = obj;
29342 }
29343 };
29344
29345 /**
29346 * Add object to the selection array.
29347 *
29348 * @param obj
29349 * @private
29350 */
29351 exports._addToHover = function(obj) {
29352 if (obj instanceof Node) {
29353 this.hoverObj.nodes[obj.id] = obj;
29354 }
29355 else {
29356 this.hoverObj.edges[obj.id] = obj;
29357 }
29358 };
29359
29360
29361 /**
29362 * Remove a single option from selection.
29363 *
29364 * @param {Object} obj
29365 * @private
29366 */
29367 exports._removeFromSelection = function(obj) {
29368 if (obj instanceof Node) {
29369 delete this.selectionObj.nodes[obj.id];
29370 }
29371 else {
29372 delete this.selectionObj.edges[obj.id];
29373 }
29374 };
29375
29376 /**
29377 * Unselect all. The selectionObj is useful for this.
29378 *
29379 * @param {Boolean} [doNotTrigger] | ignore trigger
29380 * @private
29381 */
29382 exports._unselectAll = function(doNotTrigger) {
29383 if (doNotTrigger === undefined) {
29384 doNotTrigger = false;
29385 }
29386 for(var nodeId in this.selectionObj.nodes) {
29387 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29388 this.selectionObj.nodes[nodeId].unselect();
29389 }
29390 }
29391 for(var edgeId in this.selectionObj.edges) {
29392 if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
29393 this.selectionObj.edges[edgeId].unselect();
29394 }
29395 }
29396
29397 this.selectionObj = {nodes:{},edges:{}};
29398
29399 if (doNotTrigger == false) {
29400 this.emit('select', this.getSelection());
29401 }
29402 };
29403
29404 /**
29405 * Unselect all clusters. The selectionObj is useful for this.
29406 *
29407 * @param {Boolean} [doNotTrigger] | ignore trigger
29408 * @private
29409 */
29410 exports._unselectClusters = function(doNotTrigger) {
29411 if (doNotTrigger === undefined) {
29412 doNotTrigger = false;
29413 }
29414
29415 for (var nodeId in this.selectionObj.nodes) {
29416 if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29417 if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
29418 this.selectionObj.nodes[nodeId].unselect();
29419 this._removeFromSelection(this.selectionObj.nodes[nodeId]);
29420 }
29421 }
29422 }
29423
29424 if (doNotTrigger == false) {
29425 this.emit('select', this.getSelection());
29426 }
29427 };
29428
29429
29430 /**
29431 * return the number of selected nodes
29432 *
29433 * @returns {number}
29434 * @private
29435 */
29436 exports._getSelectedNodeCount = function() {
29437 var count = 0;
29438 for (var nodeId in this.selectionObj.nodes) {
29439 if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29440 count += 1;
29441 }
29442 }
29443 return count;
29444 };
29445
29446 /**
29447 * return the selected node
29448 *
29449 * @returns {number}
29450 * @private
29451 */
29452 exports._getSelectedNode = function() {
29453 for (var nodeId in this.selectionObj.nodes) {
29454 if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29455 return this.selectionObj.nodes[nodeId];
29456 }
29457 }
29458 return null;
29459 };
29460
29461 /**
29462 * return the selected edge
29463 *
29464 * @returns {number}
29465 * @private
29466 */
29467 exports._getSelectedEdge = function() {
29468 for (var edgeId in this.selectionObj.edges) {
29469 if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
29470 return this.selectionObj.edges[edgeId];
29471 }
29472 }
29473 return null;
29474 };
29475
29476
29477 /**
29478 * return the number of selected edges
29479 *
29480 * @returns {number}
29481 * @private
29482 */
29483 exports._getSelectedEdgeCount = function() {
29484 var count = 0;
29485 for (var edgeId in this.selectionObj.edges) {
29486 if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
29487 count += 1;
29488 }
29489 }
29490 return count;
29491 };
29492
29493
29494 /**
29495 * return the number of selected objects.
29496 *
29497 * @returns {number}
29498 * @private
29499 */
29500 exports._getSelectedObjectCount = function() {
29501 var count = 0;
29502 for(var nodeId in this.selectionObj.nodes) {
29503 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29504 count += 1;
29505 }
29506 }
29507 for(var edgeId in this.selectionObj.edges) {
29508 if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
29509 count += 1;
29510 }
29511 }
29512 return count;
29513 };
29514
29515 /**
29516 * Check if anything is selected
29517 *
29518 * @returns {boolean}
29519 * @private
29520 */
29521 exports._selectionIsEmpty = function() {
29522 for(var nodeId in this.selectionObj.nodes) {
29523 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29524 return false;
29525 }
29526 }
29527 for(var edgeId in this.selectionObj.edges) {
29528 if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
29529 return false;
29530 }
29531 }
29532 return true;
29533 };
29534
29535
29536 /**
29537 * check if one of the selected nodes is a cluster.
29538 *
29539 * @returns {boolean}
29540 * @private
29541 */
29542 exports._clusterInSelection = function() {
29543 for(var nodeId in this.selectionObj.nodes) {
29544 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29545 if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
29546 return true;
29547 }
29548 }
29549 }
29550 return false;
29551 };
29552
29553 /**
29554 * select the edges connected to the node that is being selected
29555 *
29556 * @param {Node} node
29557 * @private
29558 */
29559 exports._selectConnectedEdges = function(node) {
29560 for (var i = 0; i < node.dynamicEdges.length; i++) {
29561 var edge = node.dynamicEdges[i];
29562 edge.select();
29563 this._addToSelection(edge);
29564 }
29565 };
29566
29567 /**
29568 * select the edges connected to the node that is being selected
29569 *
29570 * @param {Node} node
29571 * @private
29572 */
29573 exports._hoverConnectedEdges = function(node) {
29574 for (var i = 0; i < node.dynamicEdges.length; i++) {
29575 var edge = node.dynamicEdges[i];
29576 edge.hover = true;
29577 this._addToHover(edge);
29578 }
29579 };
29580
29581
29582 /**
29583 * unselect the edges connected to the node that is being selected
29584 *
29585 * @param {Node} node
29586 * @private
29587 */
29588 exports._unselectConnectedEdges = function(node) {
29589 for (var i = 0; i < node.dynamicEdges.length; i++) {
29590 var edge = node.dynamicEdges[i];
29591 edge.unselect();
29592 this._removeFromSelection(edge);
29593 }
29594 };
29595
29596
29597
29598
29599 /**
29600 * This is called when someone clicks on a node. either select or deselect it.
29601 * If there is an existing selection and we don't want to append to it, clear the existing selection
29602 *
29603 * @param {Node || Edge} object
29604 * @param {Boolean} append
29605 * @param {Boolean} [doNotTrigger] | ignore trigger
29606 * @private
29607 */
29608 exports._selectObject = function(object, append, doNotTrigger, highlightEdges) {
29609 if (doNotTrigger === undefined) {
29610 doNotTrigger = false;
29611 }
29612 if (highlightEdges === undefined) {
29613 highlightEdges = true;
29614 }
29615
29616 if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
29617 this._unselectAll(true);
29618 }
29619
29620 if (object.selected == false) {
29621 object.select();
29622 this._addToSelection(object);
29623 if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
29624 this._selectConnectedEdges(object);
29625 }
29626 }
29627 else {
29628 object.unselect();
29629 this._removeFromSelection(object);
29630 }
29631
29632 if (doNotTrigger == false) {
29633 this.emit('select', this.getSelection());
29634 }
29635 };
29636
29637
29638 /**
29639 * This is called when someone clicks on a node. either select or deselect it.
29640 * If there is an existing selection and we don't want to append to it, clear the existing selection
29641 *
29642 * @param {Node || Edge} object
29643 * @private
29644 */
29645 exports._blurObject = function(object) {
29646 if (object.hover == true) {
29647 object.hover = false;
29648 this.emit("blurNode",{node:object.id});
29649 }
29650 };
29651
29652 /**
29653 * This is called when someone clicks on a node. either select or deselect it.
29654 * If there is an existing selection and we don't want to append to it, clear the existing selection
29655 *
29656 * @param {Node || Edge} object
29657 * @private
29658 */
29659 exports._hoverObject = function(object) {
29660 if (object.hover == false) {
29661 object.hover = true;
29662 this._addToHover(object);
29663 if (object instanceof Node) {
29664 this.emit("hoverNode",{node:object.id});
29665 }
29666 }
29667 if (object instanceof Node) {
29668 this._hoverConnectedEdges(object);
29669 }
29670 };
29671
29672
29673 /**
29674 * handles the selection part of the touch, only for navigation controls elements;
29675 * Touch is triggered before tap, also before hold. Hold triggers after a while.
29676 * This is the most responsive solution
29677 *
29678 * @param {Object} pointer
29679 * @private
29680 */
29681 exports._handleTouch = function(pointer) {
29682 };
29683
29684
29685 /**
29686 * handles the selection part of the tap;
29687 *
29688 * @param {Object} pointer
29689 * @private
29690 */
29691 exports._handleTap = function(pointer) {
29692 var node = this._getNodeAt(pointer);
29693 if (node != null) {
29694 this._selectObject(node,false);
29695 }
29696 else {
29697 var edge = this._getEdgeAt(pointer);
29698 if (edge != null) {
29699 this._selectObject(edge,false);
29700 }
29701 else {
29702 this._unselectAll();
29703 }
29704 }
29705 this.emit("click", this.getSelection());
29706 this._redraw();
29707 };
29708
29709
29710 /**
29711 * handles the selection part of the double tap and opens a cluster if needed
29712 *
29713 * @param {Object} pointer
29714 * @private
29715 */
29716 exports._handleDoubleTap = function(pointer) {
29717 var node = this._getNodeAt(pointer);
29718 if (node != null && node !== undefined) {
29719 // we reset the areaCenter here so the opening of the node will occur
29720 this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
29721 "y" : this._YconvertDOMtoCanvas(pointer.y)};
29722 this.openCluster(node);
29723 }
29724 this.emit("doubleClick", this.getSelection());
29725 };
29726
29727
29728 /**
29729 * Handle the onHold selection part
29730 *
29731 * @param pointer
29732 * @private
29733 */
29734 exports._handleOnHold = function(pointer) {
29735 var node = this._getNodeAt(pointer);
29736 if (node != null) {
29737 this._selectObject(node,true);
29738 }
29739 else {
29740 var edge = this._getEdgeAt(pointer);
29741 if (edge != null) {
29742 this._selectObject(edge,true);
29743 }
29744 }
29745 this._redraw();
29746 };
29747
29748
29749 /**
29750 * handle the onRelease event. These functions are here for the navigation controls module.
29751 *
29752 * @private
29753 */
29754 exports._handleOnRelease = function(pointer) {
29755
29756 };
29757
29758
29759
29760 /**
29761 *
29762 * retrieve the currently selected objects
29763 * @return {{nodes: Array.<String>, edges: Array.<String>}} selection
29764 */
29765 exports.getSelection = function() {
29766 var nodeIds = this.getSelectedNodes();
29767 var edgeIds = this.getSelectedEdges();
29768 return {nodes:nodeIds, edges:edgeIds};
29769 };
29770
29771 /**
29772 *
29773 * retrieve the currently selected nodes
29774 * @return {String[]} selection An array with the ids of the
29775 * selected nodes.
29776 */
29777 exports.getSelectedNodes = function() {
29778 var idArray = [];
29779 for(var nodeId in this.selectionObj.nodes) {
29780 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29781 idArray.push(nodeId);
29782 }
29783 }
29784 return idArray
29785 };
29786
29787 /**
29788 *
29789 * retrieve the currently selected edges
29790 * @return {Array} selection An array with the ids of the
29791 * selected nodes.
29792 */
29793 exports.getSelectedEdges = function() {
29794 var idArray = [];
29795 for(var edgeId in this.selectionObj.edges) {
29796 if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
29797 idArray.push(edgeId);
29798 }
29799 }
29800 return idArray;
29801 };
29802
29803
29804 /**
29805 * select zero or more nodes
29806 * @param {Number[] | String[]} selection An array with the ids of the
29807 * selected nodes.
29808 */
29809 exports.setSelection = function(selection) {
29810 var i, iMax, id;
29811
29812 if (!selection || (selection.length == undefined))
29813 throw 'Selection must be an array with ids';
29814
29815 // first unselect any selected node
29816 this._unselectAll(true);
29817
29818 for (i = 0, iMax = selection.length; i < iMax; i++) {
29819 id = selection[i];
29820
29821 var node = this.nodes[id];
29822 if (!node) {
29823 throw new RangeError('Node with id "' + id + '" not found');
29824 }
29825 this._selectObject(node,true,true);
29826 }
29827
29828 console.log("setSelection is deprecated. Please use selectNodes instead.")
29829
29830 this.redraw();
29831 };
29832
29833
29834 /**
29835 * select zero or more nodes with the option to highlight edges
29836 * @param {Number[] | String[]} selection An array with the ids of the
29837 * selected nodes.
29838 * @param {boolean} [highlightEdges]
29839 */
29840 exports.selectNodes = function(selection, highlightEdges) {
29841 var i, iMax, id;
29842
29843 if (!selection || (selection.length == undefined))
29844 throw 'Selection must be an array with ids';
29845
29846 // first unselect any selected node
29847 this._unselectAll(true);
29848
29849 for (i = 0, iMax = selection.length; i < iMax; i++) {
29850 id = selection[i];
29851
29852 var node = this.nodes[id];
29853 if (!node) {
29854 throw new RangeError('Node with id "' + id + '" not found');
29855 }
29856 this._selectObject(node,true,true,highlightEdges);
29857 }
29858 this.redraw();
29859 };
29860
29861
29862 /**
29863 * select zero or more edges
29864 * @param {Number[] | String[]} selection An array with the ids of the
29865 * selected nodes.
29866 */
29867 exports.selectEdges = function(selection) {
29868 var i, iMax, id;
29869
29870 if (!selection || (selection.length == undefined))
29871 throw 'Selection must be an array with ids';
29872
29873 // first unselect any selected node
29874 this._unselectAll(true);
29875
29876 for (i = 0, iMax = selection.length; i < iMax; i++) {
29877 id = selection[i];
29878
29879 var edge = this.edges[id];
29880 if (!edge) {
29881 throw new RangeError('Edge with id "' + id + '" not found');
29882 }
29883 this._selectObject(edge,true,true,highlightEdges);
29884 }
29885 this.redraw();
29886 };
29887
29888 /**
29889 * Validate the selection: remove ids of nodes which no longer exist
29890 * @private
29891 */
29892 exports._updateSelection = function () {
29893 for(var nodeId in this.selectionObj.nodes) {
29894 if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
29895 if (!this.nodes.hasOwnProperty(nodeId)) {
29896 delete this.selectionObj.nodes[nodeId];
29897 }
29898 }
29899 }
29900 for(var edgeId in this.selectionObj.edges) {
29901 if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
29902 if (!this.edges.hasOwnProperty(edgeId)) {
29903 delete this.selectionObj.edges[edgeId];
29904 }
29905 }
29906 }
29907 };
29908
29909
29910/***/ },
29911/* 57 */
29912/***/ function(module, exports, __webpack_require__) {
29913
29914 var util = __webpack_require__(1);
29915 var Node = __webpack_require__(37);
29916 var Edge = __webpack_require__(34);
29917
29918 /**
29919 * clears the toolbar div element of children
29920 *
29921 * @private
29922 */
29923 exports._clearManipulatorBar = function() {
29924 while (this.manipulationDiv.hasChildNodes()) {
29925 this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
29926 }
29927 };
29928
29929 /**
29930 * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
29931 * these functions to their original functionality, we saved them in this.cachedFunctions.
29932 * This function restores these functions to their original function.
29933 *
29934 * @private
29935 */
29936 exports._restoreOverloadedFunctions = function() {
29937 for (var functionName in this.cachedFunctions) {
29938 if (this.cachedFunctions.hasOwnProperty(functionName)) {
29939 this[functionName] = this.cachedFunctions[functionName];
29940 }
29941 }
29942 };
29943
29944 /**
29945 * Enable or disable edit-mode.
29946 *
29947 * @private
29948 */
29949 exports._toggleEditMode = function() {
29950 this.editMode = !this.editMode;
29951 var toolbar = document.getElementById("network-manipulationDiv");
29952 var closeDiv = document.getElementById("network-manipulation-closeDiv");
29953 var editModeDiv = document.getElementById("network-manipulation-editMode");
29954 if (this.editMode == true) {
29955 toolbar.style.display="block";
29956 closeDiv.style.display="block";
29957 editModeDiv.style.display="none";
29958 closeDiv.onclick = this._toggleEditMode.bind(this);
29959 }
29960 else {
29961 toolbar.style.display="none";
29962 closeDiv.style.display="none";
29963 editModeDiv.style.display="block";
29964 closeDiv.onclick = null;
29965 }
29966 this._createManipulatorBar()
29967 };
29968
29969 /**
29970 * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
29971 *
29972 * @private
29973 */
29974 exports._createManipulatorBar = function() {
29975 // remove bound functions
29976 if (this.boundFunction) {
29977 this.off('select', this.boundFunction);
29978 }
29979
29980 var locale = this.constants.locales[this.constants.locale];
29981
29982 if (this.edgeBeingEdited !== undefined) {
29983 this.edgeBeingEdited._disableControlNodes();
29984 this.edgeBeingEdited = undefined;
29985 this.selectedControlNode = null;
29986 this.controlNodesActive = false;
29987 }
29988
29989 // restore overloaded functions
29990 this._restoreOverloadedFunctions();
29991
29992 // resume calculation
29993 this.freezeSimulation = false;
29994
29995 // reset global variables
29996 this.blockConnectingEdgeSelection = false;
29997 this.forceAppendSelection = false;
29998
29999 if (this.editMode == true) {
30000 while (this.manipulationDiv.hasChildNodes()) {
30001 this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
30002 }
30003
30004 // add the icons to the manipulator div
30005 this.manipulationDiv.innerHTML = "" +
30006 "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
30007 "<span class='network-manipulationLabel'>"+locale['addNode'] +"</span></span>" +
30008 "<div class='network-seperatorLine'></div>" +
30009 "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
30010 "<span class='network-manipulationLabel'>"+locale['addEdge'] +"</span></span>";
30011 if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
30012 this.manipulationDiv.innerHTML += "" +
30013 "<div class='network-seperatorLine'></div>" +
30014 "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
30015 "<span class='network-manipulationLabel'>"+locale['editNode'] +"</span></span>";
30016 }
30017 else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
30018 this.manipulationDiv.innerHTML += "" +
30019 "<div class='network-seperatorLine'></div>" +
30020 "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
30021 "<span class='network-manipulationLabel'>"+locale['editEdge'] +"</span></span>";
30022 }
30023 if (this._selectionIsEmpty() == false) {
30024 this.manipulationDiv.innerHTML += "" +
30025 "<div class='network-seperatorLine'></div>" +
30026 "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
30027 "<span class='network-manipulationLabel'>"+locale['del'] +"</span></span>";
30028 }
30029
30030
30031 // bind the icons
30032 var addNodeButton = document.getElementById("network-manipulate-addNode");
30033 addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
30034 var addEdgeButton = document.getElementById("network-manipulate-connectNode");
30035 addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
30036 if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
30037 var editButton = document.getElementById("network-manipulate-editNode");
30038 editButton.onclick = this._editNode.bind(this);
30039 }
30040 else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
30041 var editButton = document.getElementById("network-manipulate-editEdge");
30042 editButton.onclick = this._createEditEdgeToolbar.bind(this);
30043 }
30044 if (this._selectionIsEmpty() == false) {
30045 var deleteButton = document.getElementById("network-manipulate-delete");
30046 deleteButton.onclick = this._deleteSelected.bind(this);
30047 }
30048 var closeDiv = document.getElementById("network-manipulation-closeDiv");
30049 closeDiv.onclick = this._toggleEditMode.bind(this);
30050
30051 this.boundFunction = this._createManipulatorBar.bind(this);
30052 this.on('select', this.boundFunction);
30053 }
30054 else {
30055 this.editModeDiv.innerHTML = "" +
30056 "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
30057 "<span class='network-manipulationLabel'>" + locale['edit'] + "</span></span>";
30058 var editModeButton = document.getElementById("network-manipulate-editModeButton");
30059 editModeButton.onclick = this._toggleEditMode.bind(this);
30060 }
30061 };
30062
30063
30064
30065 /**
30066 * Create the toolbar for adding Nodes
30067 *
30068 * @private
30069 */
30070 exports._createAddNodeToolbar = function() {
30071 // clear the toolbar
30072 this._clearManipulatorBar();
30073 if (this.boundFunction) {
30074 this.off('select', this.boundFunction);
30075 }
30076
30077 var locale = this.constants.locales[this.constants.locale];
30078
30079 // create the toolbar contents
30080 this.manipulationDiv.innerHTML = "" +
30081 "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
30082 "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
30083 "<div class='network-seperatorLine'></div>" +
30084 "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
30085 "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['addDescription'] + "</span></span>";
30086
30087 // bind the icon
30088 var backButton = document.getElementById("network-manipulate-back");
30089 backButton.onclick = this._createManipulatorBar.bind(this);
30090
30091 // we use the boundFunction so we can reference it when we unbind it from the "select" event.
30092 this.boundFunction = this._addNode.bind(this);
30093 this.on('select', this.boundFunction);
30094 };
30095
30096
30097 /**
30098 * create the toolbar to connect nodes
30099 *
30100 * @private
30101 */
30102 exports._createAddEdgeToolbar = function() {
30103 // clear the toolbar
30104 this._clearManipulatorBar();
30105 this._unselectAll(true);
30106 this.freezeSimulation = true;
30107
30108 var locale = this.constants.locales[this.constants.locale];
30109
30110 if (this.boundFunction) {
30111 this.off('select', this.boundFunction);
30112 }
30113
30114 this._unselectAll();
30115 this.forceAppendSelection = false;
30116 this.blockConnectingEdgeSelection = true;
30117
30118 this.manipulationDiv.innerHTML = "" +
30119 "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
30120 "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
30121 "<div class='network-seperatorLine'></div>" +
30122 "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
30123 "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['edgeDescription'] + "</span></span>";
30124
30125 // bind the icon
30126 var backButton = document.getElementById("network-manipulate-back");
30127 backButton.onclick = this._createManipulatorBar.bind(this);
30128
30129 // we use the boundFunction so we can reference it when we unbind it from the "select" event.
30130 this.boundFunction = this._handleConnect.bind(this);
30131 this.on('select', this.boundFunction);
30132
30133 // temporarily overload functions
30134 this.cachedFunctions["_handleTouch"] = this._handleTouch;
30135 this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
30136 this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
30137 this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd;
30138 this._handleTouch = this._handleConnect;
30139 this._handleOnRelease = function () {};
30140 this._handleDragStart = function () {};
30141 this._handleDragEnd = this._finishConnect;
30142
30143 // redraw to show the unselect
30144 this._redraw();
30145 };
30146
30147 /**
30148 * create the toolbar to edit edges
30149 *
30150 * @private
30151 */
30152 exports._createEditEdgeToolbar = function() {
30153 // clear the toolbar
30154 this._clearManipulatorBar();
30155 this.controlNodesActive = true;
30156
30157 if (this.boundFunction) {
30158 this.off('select', this.boundFunction);
30159 }
30160
30161 this.edgeBeingEdited = this._getSelectedEdge();
30162 this.edgeBeingEdited._enableControlNodes();
30163
30164 var locale = this.constants.locales[this.constants.locale];
30165
30166 this.manipulationDiv.innerHTML = "" +
30167 "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
30168 "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
30169 "<div class='network-seperatorLine'></div>" +
30170 "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
30171 "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['editEdgeDescription'] + "</span></span>";
30172
30173 // bind the icon
30174 var backButton = document.getElementById("network-manipulate-back");
30175 backButton.onclick = this._createManipulatorBar.bind(this);
30176
30177 // temporarily overload functions
30178 this.cachedFunctions["_handleTouch"] = this._handleTouch;
30179 this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
30180 this.cachedFunctions["_handleTap"] = this._handleTap;
30181 this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
30182 this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
30183 this._handleTouch = this._selectControlNode;
30184 this._handleTap = function () {};
30185 this._handleOnDrag = this._controlNodeDrag;
30186 this._handleDragStart = function () {}
30187 this._handleOnRelease = this._releaseControlNode;
30188
30189 // redraw to show the unselect
30190 this._redraw();
30191 };
30192
30193
30194 /**
30195 * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
30196 * to walk the user through the process.
30197 *
30198 * @private
30199 */
30200 exports._selectControlNode = function(pointer) {
30201 this.edgeBeingEdited.controlNodes.from.unselect();
30202 this.edgeBeingEdited.controlNodes.to.unselect();
30203 this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
30204 if (this.selectedControlNode !== null) {
30205 this.selectedControlNode.select();
30206 this.freezeSimulation = true;
30207 }
30208 this._redraw();
30209 };
30210
30211
30212 /**
30213 * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
30214 * to walk the user through the process.
30215 *
30216 * @private
30217 */
30218 exports._controlNodeDrag = function(event) {
30219 var pointer = this._getPointer(event.gesture.center);
30220 if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
30221 this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
30222 this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
30223 }
30224 this._redraw();
30225 };
30226
30227 exports._releaseControlNode = function(pointer) {
30228 var newNode = this._getNodeAt(pointer);
30229 if (newNode != null) {
30230 if (this.edgeBeingEdited.controlNodes.from.selected == true) {
30231 this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
30232 this.edgeBeingEdited.controlNodes.from.unselect();
30233 }
30234 if (this.edgeBeingEdited.controlNodes.to.selected == true) {
30235 this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
30236 this.edgeBeingEdited.controlNodes.to.unselect();
30237 }
30238 }
30239 else {
30240 this.edgeBeingEdited._restoreControlNodes();
30241 }
30242 this.freezeSimulation = false;
30243 this._redraw();
30244 };
30245
30246 /**
30247 * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
30248 * to walk the user through the process.
30249 *
30250 * @private
30251 */
30252 exports._handleConnect = function(pointer) {
30253 if (this._getSelectedNodeCount() == 0) {
30254 var node = this._getNodeAt(pointer);
30255
30256 if (node != null) {
30257 if (node.clusterSize > 1) {
30258 alert(this.constants.locales[this.constants.locale]['createEdgeError'])
30259 }
30260 else {
30261 this._selectObject(node,false);
30262 var supportNodes = this.sectors['support']['nodes'];
30263
30264 // create a node the temporary line can look at
30265 supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
30266 var targetNode = supportNodes['targetNode'];
30267 targetNode.x = node.x;
30268 targetNode.y = node.y;
30269
30270 // create a temporary edge
30271 this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
30272 var connectionEdge = this.edges['connectionEdge'];
30273 connectionEdge.from = node;
30274 connectionEdge.connected = true;
30275 connectionEdge.options.smoothCurves = {enabled: true,
30276 dynamic: false,
30277 type: "continuous",
30278 roundness: 0.5
30279 };
30280 connectionEdge.selected = true;
30281 connectionEdge.to = targetNode;
30282
30283 this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
30284 this._handleOnDrag = function(event) {
30285 var pointer = this._getPointer(event.gesture.center);
30286 var connectionEdge = this.edges['connectionEdge'];
30287 connectionEdge.to.x = this._XconvertDOMtoCanvas(pointer.x);
30288 connectionEdge.to.y = this._YconvertDOMtoCanvas(pointer.y);
30289 };
30290
30291 this.moving = true;
30292 this.start();
30293 }
30294 }
30295 }
30296 };
30297
30298 exports._finishConnect = function(event) {
30299 if (this._getSelectedNodeCount() == 1) {
30300 var pointer = this._getPointer(event.gesture.center);
30301 // restore the drag function
30302 this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
30303 delete this.cachedFunctions["_handleOnDrag"];
30304
30305 // remember the edge id
30306 var connectFromId = this.edges['connectionEdge'].fromId;
30307
30308 // remove the temporary nodes and edge
30309 delete this.edges['connectionEdge'];
30310 delete this.sectors['support']['nodes']['targetNode'];
30311 delete this.sectors['support']['nodes']['targetViaNode'];
30312
30313 var node = this._getNodeAt(pointer);
30314 if (node != null) {
30315 if (node.clusterSize > 1) {
30316 alert(this.constants.locales[this.constants.locale]["createEdgeError"])
30317 }
30318 else {
30319 this._createEdge(connectFromId,node.id);
30320 this._createManipulatorBar();
30321 }
30322 }
30323 this._unselectAll();
30324 }
30325 };
30326
30327
30328 /**
30329 * Adds a node on the specified location
30330 */
30331 exports._addNode = function() {
30332 if (this._selectionIsEmpty() && this.editMode == true) {
30333 var positionObject = this._pointerToPositionObject(this.pointerPosition);
30334 var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
30335 if (this.triggerFunctions.add) {
30336 if (this.triggerFunctions.add.length == 2) {
30337 var me = this;
30338 this.triggerFunctions.add(defaultData, function(finalizedData) {
30339 me.nodesData.add(finalizedData);
30340 me._createManipulatorBar();
30341 me.moving = true;
30342 me.start();
30343 });
30344 }
30345 else {
30346 throw new Error('The function for add does not support two arguments (data,callback)');
30347 this._createManipulatorBar();
30348 this.moving = true;
30349 this.start();
30350 }
30351 }
30352 else {
30353 this.nodesData.add(defaultData);
30354 this._createManipulatorBar();
30355 this.moving = true;
30356 this.start();
30357 }
30358 }
30359 };
30360
30361
30362 /**
30363 * connect two nodes with a new edge.
30364 *
30365 * @private
30366 */
30367 exports._createEdge = function(sourceNodeId,targetNodeId) {
30368 if (this.editMode == true) {
30369 var defaultData = {from:sourceNodeId, to:targetNodeId};
30370 if (this.triggerFunctions.connect) {
30371 if (this.triggerFunctions.connect.length == 2) {
30372 var me = this;
30373 this.triggerFunctions.connect(defaultData, function(finalizedData) {
30374 me.edgesData.add(finalizedData);
30375 me.moving = true;
30376 me.start();
30377 });
30378 }
30379 else {
30380 throw new Error('The function for connect does not support two arguments (data,callback)');
30381 this.moving = true;
30382 this.start();
30383 }
30384 }
30385 else {
30386 this.edgesData.add(defaultData);
30387 this.moving = true;
30388 this.start();
30389 }
30390 }
30391 };
30392
30393 /**
30394 * connect two nodes with a new edge.
30395 *
30396 * @private
30397 */
30398 exports._editEdge = function(sourceNodeId,targetNodeId) {
30399 if (this.editMode == true) {
30400 var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
30401 if (this.triggerFunctions.editEdge) {
30402 if (this.triggerFunctions.editEdge.length == 2) {
30403 var me = this;
30404 this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
30405 me.edgesData.update(finalizedData);
30406 me.moving = true;
30407 me.start();
30408 });
30409 }
30410 else {
30411 throw new Error('The function for edit does not support two arguments (data, callback)');
30412 this.moving = true;
30413 this.start();
30414 }
30415 }
30416 else {
30417 this.edgesData.update(defaultData);
30418 this.moving = true;
30419 this.start();
30420 }
30421 }
30422 };
30423
30424 /**
30425 * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
30426 *
30427 * @private
30428 */
30429 exports._editNode = function() {
30430 if (this.triggerFunctions.edit && this.editMode == true) {
30431 var node = this._getSelectedNode();
30432 var data = {id:node.id,
30433 label: node.label,
30434 group: node.options.group,
30435 shape: node.options.shape,
30436 color: {
30437 background:node.options.color.background,
30438 border:node.options.color.border,
30439 highlight: {
30440 background:node.options.color.highlight.background,
30441 border:node.options.color.highlight.border
30442 }
30443 }};
30444 if (this.triggerFunctions.edit.length == 2) {
30445 var me = this;
30446 this.triggerFunctions.edit(data, function (finalizedData) {
30447 me.nodesData.update(finalizedData);
30448 me._createManipulatorBar();
30449 me.moving = true;
30450 me.start();
30451 });
30452 }
30453 else {
30454 throw new Error('The function for edit does not support two arguments (data, callback)');
30455 }
30456 }
30457 else {
30458 throw new Error('No edit function has been bound to this button');
30459 }
30460 };
30461
30462
30463
30464
30465 /**
30466 * delete everything in the selection
30467 *
30468 * @private
30469 */
30470 exports._deleteSelected = function() {
30471 if (!this._selectionIsEmpty() && this.editMode == true) {
30472 if (!this._clusterInSelection()) {
30473 var selectedNodes = this.getSelectedNodes();
30474 var selectedEdges = this.getSelectedEdges();
30475 if (this.triggerFunctions.del) {
30476 var me = this;
30477 var data = {nodes: selectedNodes, edges: selectedEdges};
30478 if (this.triggerFunctions.del.length = 2) {
30479 this.triggerFunctions.del(data, function (finalizedData) {
30480 me.edgesData.remove(finalizedData.edges);
30481 me.nodesData.remove(finalizedData.nodes);
30482 me._unselectAll();
30483 me.moving = true;
30484 me.start();
30485 });
30486 }
30487 else {
30488 throw new Error('The function for delete does not support two arguments (data, callback)')
30489 }
30490 }
30491 else {
30492 this.edgesData.remove(selectedEdges);
30493 this.nodesData.remove(selectedNodes);
30494 this._unselectAll();
30495 this.moving = true;
30496 this.start();
30497 }
30498 }
30499 else {
30500 alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
30501 }
30502 }
30503 };
30504
30505
30506/***/ },
30507/* 58 */
30508/***/ function(module, exports, __webpack_require__) {
30509
30510 var util = __webpack_require__(1);
30511 var Hammer = __webpack_require__(42);
30512
30513 exports._cleanNavigation = function() {
30514 // clean hammer bindings
30515 if (this.navigationHammers.existing.length != 0) {
30516 for (var i = 0; i < this.navigationHammers.existing.length; i++) {
30517 this.navigationHammers.existing[i].dispose();
30518 }
30519 this.navigationHammers.existing = [];
30520 }
30521
30522 // clean up previous navigation items
30523 var wrapper = document.getElementById('network-navigation_wrapper');
30524 if (wrapper && wrapper.parentNode) {
30525 wrapper.parentNode.removeChild(wrapper);
30526 }
30527 };
30528
30529 /**
30530 * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
30531 * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
30532 * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
30533 * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
30534 *
30535 * @private
30536 */
30537 exports._loadNavigationElements = function() {
30538 this._cleanNavigation();
30539
30540 this.navigationDivs = {};
30541 var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
30542 var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent'];
30543
30544 this.navigationDivs['wrapper'] = document.createElement('div');
30545 this.navigationDivs['wrapper'].id = 'network-navigation_wrapper';
30546 this.frame.appendChild(this.navigationDivs['wrapper']);
30547
30548 for (var i = 0; i < navigationDivs.length; i++) {
30549 this.navigationDivs[navigationDivs[i]] = document.createElement('div');
30550 this.navigationDivs[navigationDivs[i]].id = 'network-navigation_' + navigationDivs[i];
30551 this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i];
30552 this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
30553
30554 var hammer = Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true});
30555 hammer.on('touch', this[navigationDivActions[i]].bind(this));
30556 this.navigationHammers.new.push(hammer);
30557 }
30558
30559 var hammerDoc = Hammer(document, {prevent_default: false});
30560 hammerDoc.on('release', this._stopMovement.bind(this));
30561 this.navigationHammers.new.push(hammerDoc);
30562
30563 this.navigationHammers.existing = this.navigationHammers.new;
30564 };
30565
30566
30567 /**
30568 * this stops all movement induced by the navigation buttons
30569 *
30570 * @private
30571 */
30572 exports._zoomExtent = function(event) {
30573 // FIXME: this is a workaround because the binding of Hammer on Document makes this fire twice
30574 if (this._zoomExtentLastTime === undefined || new Date() - this._zoomExtentLastTime > 200) {
30575 this._zoomExtentLastTime = new Date();
30576 this.zoomExtent({duration:800});
30577 event.stopPropagation();
30578 }
30579 };
30580
30581 /**
30582 * this stops all movement induced by the navigation buttons
30583 *
30584 * @private
30585 */
30586 exports._stopMovement = function() {
30587 this._xStopMoving();
30588 this._yStopMoving();
30589 this._stopZoom();
30590 };
30591
30592
30593 /**
30594 * move the screen up
30595 * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
30596 * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
30597 * To avoid this behaviour, we do the translation in the start loop.
30598 *
30599 * @private
30600 */
30601 exports._moveUp = function(event) {
30602 this.yIncrement = this.constants.keyboard.speed.y;
30603 this.start(); // if there is no node movement, the calculation wont be done
30604 event.preventDefault();
30605 };
30606
30607
30608 /**
30609 * move the screen down
30610 * @private
30611 */
30612 exports._moveDown = function(event) {
30613 this.yIncrement = -this.constants.keyboard.speed.y;
30614 this.start(); // if there is no node movement, the calculation wont be done
30615 event.preventDefault();
30616 };
30617
30618
30619 /**
30620 * move the screen left
30621 * @private
30622 */
30623 exports._moveLeft = function(event) {
30624 this.xIncrement = this.constants.keyboard.speed.x;
30625 this.start(); // if there is no node movement, the calculation wont be done
30626 event.preventDefault();
30627 };
30628
30629
30630 /**
30631 * move the screen right
30632 * @private
30633 */
30634 exports._moveRight = function(event) {
30635 this.xIncrement = -this.constants.keyboard.speed.y;
30636 this.start(); // if there is no node movement, the calculation wont be done
30637 event.preventDefault();
30638 };
30639
30640
30641 /**
30642 * Zoom in, using the same method as the movement.
30643 * @private
30644 */
30645 exports._zoomIn = function(event) {
30646 this.zoomIncrement = this.constants.keyboard.speed.zoom;
30647 this.start(); // if there is no node movement, the calculation wont be done
30648 event.preventDefault();
30649 };
30650
30651
30652 /**
30653 * Zoom out
30654 * @private
30655 */
30656 exports._zoomOut = function(event) {
30657 this.zoomIncrement = -this.constants.keyboard.speed.zoom;
30658 this.start(); // if there is no node movement, the calculation wont be done
30659 event.preventDefault();
30660 };
30661
30662
30663 /**
30664 * Stop zooming and unhighlight the zoom controls
30665 * @private
30666 */
30667 exports._stopZoom = function(event) {
30668 this.zoomIncrement = 0;
30669 event && event.preventDefault();
30670 };
30671
30672
30673 /**
30674 * Stop moving in the Y direction and unHighlight the up and down
30675 * @private
30676 */
30677 exports._yStopMoving = function(event) {
30678 this.yIncrement = 0;
30679 event && event.preventDefault();
30680 };
30681
30682
30683 /**
30684 * Stop moving in the X direction and unHighlight left and right.
30685 * @private
30686 */
30687 exports._xStopMoving = function(event) {
30688 this.xIncrement = 0;
30689 event && event.preventDefault();
30690 };
30691
30692
30693/***/ },
30694/* 59 */
30695/***/ function(module, exports, __webpack_require__) {
30696
30697 exports._resetLevels = function() {
30698 for (var nodeId in this.nodes) {
30699 if (this.nodes.hasOwnProperty(nodeId)) {
30700 var node = this.nodes[nodeId];
30701 if (node.preassignedLevel == false) {
30702 node.level = -1;
30703 node.hierarchyEnumerated = false;
30704 }
30705 }
30706 }
30707 };
30708
30709 /**
30710 * This is the main function to layout the nodes in a hierarchical way.
30711 * It checks if the node details are supplied correctly
30712 *
30713 * @private
30714 */
30715 exports._setupHierarchicalLayout = function() {
30716 if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
30717 if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
30718 this.constants.hierarchicalLayout.levelSeparation *= -1;
30719 }
30720 else {
30721 this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
30722 }
30723
30724 if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") {
30725 if (this.constants.smoothCurves.enabled == true) {
30726 this.constants.smoothCurves.type = "vertical";
30727 }
30728 }
30729 else {
30730 if (this.constants.smoothCurves.enabled == true) {
30731 this.constants.smoothCurves.type = "horizontal";
30732 }
30733 }
30734 // get the size of the largest hubs and check if the user has defined a level for a node.
30735 var hubsize = 0;
30736 var node, nodeId;
30737 var definedLevel = false;
30738 var undefinedLevel = false;
30739
30740 for (nodeId in this.nodes) {
30741 if (this.nodes.hasOwnProperty(nodeId)) {
30742 node = this.nodes[nodeId];
30743 if (node.level != -1) {
30744 definedLevel = true;
30745 }
30746 else {
30747 undefinedLevel = true;
30748 }
30749 if (hubsize < node.edges.length) {
30750 hubsize = node.edges.length;
30751 }
30752 }
30753 }
30754
30755 // if the user defined some levels but not all, alert and run without hierarchical layout
30756 if (undefinedLevel == true && definedLevel == true) {
30757 throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
30758 this.zoomExtent(undefined,true,this.constants.clustering.enabled);
30759 if (!this.constants.clustering.enabled) {
30760 this.start();
30761 }
30762 }
30763 else {
30764 // setup the system to use hierarchical method.
30765 this._changeConstants();
30766
30767 // define levels if undefined by the users. Based on hubsize
30768 if (undefinedLevel == true) {
30769 if (this.constants.hierarchicalLayout.layout == "hubsize") {
30770 this._determineLevels(hubsize);
30771 }
30772 else {
30773 this._determineLevelsDirected();
30774 }
30775
30776 }
30777 // check the distribution of the nodes per level.
30778 var distribution = this._getDistribution();
30779
30780 // place the nodes on the canvas. This also stablilizes the system.
30781 this._placeNodesByHierarchy(distribution);
30782
30783 // start the simulation.
30784 this.start();
30785 }
30786 }
30787 };
30788
30789
30790 /**
30791 * This function places the nodes on the canvas based on the hierarchial distribution.
30792 *
30793 * @param {Object} distribution | obtained by the function this._getDistribution()
30794 * @private
30795 */
30796 exports._placeNodesByHierarchy = function(distribution) {
30797 var nodeId, node;
30798
30799 // start placing all the level 0 nodes first. Then recursively position their branches.
30800 for (var level in distribution) {
30801 if (distribution.hasOwnProperty(level)) {
30802
30803 for (nodeId in distribution[level].nodes) {
30804 if (distribution[level].nodes.hasOwnProperty(nodeId)) {
30805 node = distribution[level].nodes[nodeId];
30806 if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
30807 if (node.xFixed) {
30808 node.x = distribution[level].minPos;
30809 node.xFixed = false;
30810
30811 distribution[level].minPos += distribution[level].nodeSpacing;
30812 }
30813 }
30814 else {
30815 if (node.yFixed) {
30816 node.y = distribution[level].minPos;
30817 node.yFixed = false;
30818
30819 distribution[level].minPos += distribution[level].nodeSpacing;
30820 }
30821 }
30822 this._placeBranchNodes(node.edges,node.id,distribution,node.level);
30823 }
30824 }
30825 }
30826 }
30827
30828 // stabilize the system after positioning. This function calls zoomExtent.
30829 this._stabilize();
30830 };
30831
30832
30833 /**
30834 * This function get the distribution of levels based on hubsize
30835 *
30836 * @returns {Object}
30837 * @private
30838 */
30839 exports._getDistribution = function() {
30840 var distribution = {};
30841 var nodeId, node, level;
30842
30843 // 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.
30844 // the fix of X is removed after the x value has been set.
30845 for (nodeId in this.nodes) {
30846 if (this.nodes.hasOwnProperty(nodeId)) {
30847 node = this.nodes[nodeId];
30848 node.xFixed = true;
30849 node.yFixed = true;
30850 if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
30851 node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
30852 }
30853 else {
30854 node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
30855 }
30856 if (distribution[node.level] === undefined) {
30857 distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
30858 }
30859 distribution[node.level].amount += 1;
30860 distribution[node.level].nodes[nodeId] = node;
30861 }
30862 }
30863
30864 // determine the largest amount of nodes of all levels
30865 var maxCount = 0;
30866 for (level in distribution) {
30867 if (distribution.hasOwnProperty(level)) {
30868 if (maxCount < distribution[level].amount) {
30869 maxCount = distribution[level].amount;
30870 }
30871 }
30872 }
30873
30874 // set the initial position and spacing of each nodes accordingly
30875 for (level in distribution) {
30876 if (distribution.hasOwnProperty(level)) {
30877 distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
30878 distribution[level].nodeSpacing /= (distribution[level].amount + 1);
30879 distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
30880 }
30881 }
30882
30883 return distribution;
30884 };
30885
30886
30887 /**
30888 * this function allocates nodes in levels based on the recursive branching from the largest hubs.
30889 *
30890 * @param hubsize
30891 * @private
30892 */
30893 exports._determineLevels = function(hubsize) {
30894 var nodeId, node;
30895
30896 // determine hubs
30897 for (nodeId in this.nodes) {
30898 if (this.nodes.hasOwnProperty(nodeId)) {
30899 node = this.nodes[nodeId];
30900 if (node.edges.length == hubsize) {
30901 node.level = 0;
30902 }
30903 }
30904 }
30905
30906 // branch from hubs
30907 for (nodeId in this.nodes) {
30908 if (this.nodes.hasOwnProperty(nodeId)) {
30909 node = this.nodes[nodeId];
30910 if (node.level == 0) {
30911 this._setLevel(1,node.edges,node.id);
30912 }
30913 }
30914 }
30915 };
30916
30917 /**
30918 * this function allocates nodes in levels based on the recursive branching from the largest hubs.
30919 *
30920 * @param hubsize
30921 * @private
30922 */
30923 exports._determineLevelsDirected = function() {
30924 var nodeId, node;
30925
30926 // set first node to source
30927 for (nodeId in this.nodes) {
30928 if (this.nodes.hasOwnProperty(nodeId)) {
30929 this.nodes[nodeId].level = 10000;
30930 break;
30931 }
30932 }
30933
30934 // branch from hubs
30935 for (nodeId in this.nodes) {
30936 if (this.nodes.hasOwnProperty(nodeId)) {
30937 node = this.nodes[nodeId];
30938 if (node.level == 10000) {
30939 this._setLevelDirected(10000,node.edges,node.id);
30940 }
30941 }
30942 }
30943
30944
30945 // branch from hubs
30946 var minLevel = 10000;
30947 for (nodeId in this.nodes) {
30948 if (this.nodes.hasOwnProperty(nodeId)) {
30949 node = this.nodes[nodeId];
30950 minLevel = node.level < minLevel ? node.level : minLevel;
30951 }
30952 }
30953
30954 // branch from hubs
30955 for (nodeId in this.nodes) {
30956 if (this.nodes.hasOwnProperty(nodeId)) {
30957 node = this.nodes[nodeId];
30958 node.level -= minLevel;
30959 }
30960 }
30961 };
30962
30963
30964 /**
30965 * Since hierarchical layout does not support:
30966 * - smooth curves (based on the physics),
30967 * - clustering (based on dynamic node counts)
30968 *
30969 * We disable both features so there will be no problems.
30970 *
30971 * @private
30972 */
30973 exports._changeConstants = function() {
30974 this.constants.clustering.enabled = false;
30975 this.constants.physics.barnesHut.enabled = false;
30976 this.constants.physics.hierarchicalRepulsion.enabled = true;
30977 this._loadSelectedForceSolver();
30978 if (this.constants.smoothCurves.enabled == true) {
30979 this.constants.smoothCurves.dynamic = false;
30980 }
30981 this._configureSmoothCurves();
30982 };
30983
30984
30985 /**
30986 * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
30987 * on a X position that ensures there will be no overlap.
30988 *
30989 * @param edges
30990 * @param parentId
30991 * @param distribution
30992 * @param parentLevel
30993 * @private
30994 */
30995 exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
30996 for (var i = 0; i < edges.length; i++) {
30997 var childNode = null;
30998 if (edges[i].toId == parentId) {
30999 childNode = edges[i].from;
31000 }
31001 else {
31002 childNode = edges[i].to;
31003 }
31004
31005 // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
31006 var nodeMoved = false;
31007 if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
31008 if (childNode.xFixed && childNode.level > parentLevel) {
31009 childNode.xFixed = false;
31010 childNode.x = distribution[childNode.level].minPos;
31011 nodeMoved = true;
31012 }
31013 }
31014 else {
31015 if (childNode.yFixed && childNode.level > parentLevel) {
31016 childNode.yFixed = false;
31017 childNode.y = distribution[childNode.level].minPos;
31018 nodeMoved = true;
31019 }
31020 }
31021
31022 if (nodeMoved == true) {
31023 distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
31024 if (childNode.edges.length > 1) {
31025 this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
31026 }
31027 }
31028 }
31029 };
31030
31031
31032 /**
31033 * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
31034 *
31035 * @param level
31036 * @param edges
31037 * @param parentId
31038 * @private
31039 */
31040 exports._setLevel = function(level, edges, parentId) {
31041 for (var i = 0; i < edges.length; i++) {
31042 var childNode = null;
31043 if (edges[i].toId == parentId) {
31044 childNode = edges[i].from;
31045 }
31046 else {
31047 childNode = edges[i].to;
31048 }
31049 if (childNode.level == -1 || childNode.level > level) {
31050 childNode.level = level;
31051 if (childNode.edges.length > 1) {
31052 this._setLevel(level+1, childNode.edges, childNode.id);
31053 }
31054 }
31055 }
31056 };
31057
31058
31059 /**
31060 * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
31061 *
31062 * @param level
31063 * @param edges
31064 * @param parentId
31065 * @private
31066 */
31067 exports._setLevelDirected = function(level, edges, parentId) {
31068 this.nodes[parentId].hierarchyEnumerated = true;
31069 for (var i = 0; i < edges.length; i++) {
31070 var childNode = null;
31071 var direction = 1;
31072 if (edges[i].toId == parentId) {
31073 childNode = edges[i].from;
31074 direction = -1;
31075 }
31076 else {
31077 childNode = edges[i].to;
31078 }
31079 if (childNode.level == -1) {
31080 childNode.level = level + direction;
31081 }
31082 }
31083
31084 for (var i = 0; i < edges.length; i++) {
31085 var childNode = null;
31086 if (edges[i].toId == parentId) {childNode = edges[i].from;}
31087 else {childNode = edges[i].to;}
31088 if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
31089 this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
31090 }
31091 }
31092 };
31093
31094
31095 /**
31096 * Unfix nodes
31097 *
31098 * @private
31099 */
31100 exports._restoreNodes = function() {
31101 for (var nodeId in this.nodes) {
31102 if (this.nodes.hasOwnProperty(nodeId)) {
31103 this.nodes[nodeId].xFixed = false;
31104 this.nodes[nodeId].yFixed = false;
31105 }
31106 }
31107 };
31108
31109
31110/***/ },
31111/* 60 */
31112/***/ function(module, exports, __webpack_require__) {
31113
31114 var util = __webpack_require__(1);
31115 var RepulsionMixin = __webpack_require__(62);
31116 var HierarchialRepulsionMixin = __webpack_require__(63);
31117 var BarnesHutMixin = __webpack_require__(64);
31118
31119 /**
31120 * Toggling barnes Hut calculation on and off.
31121 *
31122 * @private
31123 */
31124 exports._toggleBarnesHut = function () {
31125 this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
31126 this._loadSelectedForceSolver();
31127 this.moving = true;
31128 this.start();
31129 };
31130
31131
31132 /**
31133 * This loads the node force solver based on the barnes hut or repulsion algorithm
31134 *
31135 * @private
31136 */
31137 exports._loadSelectedForceSolver = function () {
31138 // this overloads the this._calculateNodeForces
31139 if (this.constants.physics.barnesHut.enabled == true) {
31140 this._clearMixin(RepulsionMixin);
31141 this._clearMixin(HierarchialRepulsionMixin);
31142
31143 this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
31144 this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
31145 this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
31146 this.constants.physics.damping = this.constants.physics.barnesHut.damping;
31147
31148 this._loadMixin(BarnesHutMixin);
31149 }
31150 else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
31151 this._clearMixin(BarnesHutMixin);
31152 this._clearMixin(RepulsionMixin);
31153
31154 this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
31155 this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
31156 this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
31157 this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
31158
31159 this._loadMixin(HierarchialRepulsionMixin);
31160 }
31161 else {
31162 this._clearMixin(BarnesHutMixin);
31163 this._clearMixin(HierarchialRepulsionMixin);
31164 this.barnesHutTree = undefined;
31165
31166 this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
31167 this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
31168 this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
31169 this.constants.physics.damping = this.constants.physics.repulsion.damping;
31170
31171 this._loadMixin(RepulsionMixin);
31172 }
31173 };
31174
31175 /**
31176 * Before calculating the forces, we check if we need to cluster to keep up performance and we check
31177 * if there is more than one node. If it is just one node, we dont calculate anything.
31178 *
31179 * @private
31180 */
31181 exports._initializeForceCalculation = function () {
31182 // stop calculation if there is only one node
31183 if (this.nodeIndices.length == 1) {
31184 this.nodes[this.nodeIndices[0]]._setForce(0, 0);
31185 }
31186 else {
31187 // if there are too many nodes on screen, we cluster without repositioning
31188 if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
31189 this.clusterToFit(this.constants.clustering.reduceToNodes, false);
31190 }
31191
31192 // we now start the force calculation
31193 this._calculateForces();
31194 }
31195 };
31196
31197
31198 /**
31199 * Calculate the external forces acting on the nodes
31200 * Forces are caused by: edges, repulsing forces between nodes, gravity
31201 * @private
31202 */
31203 exports._calculateForces = function () {
31204 // Gravity is required to keep separated groups from floating off
31205 // the forces are reset to zero in this loop by using _setForce instead
31206 // of _addForce
31207
31208 this._calculateGravitationalForces();
31209 this._calculateNodeForces();
31210
31211 if (this.constants.physics.springConstant > 0) {
31212 if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
31213 this._calculateSpringForcesWithSupport();
31214 }
31215 else {
31216 if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
31217 this._calculateHierarchicalSpringForces();
31218 }
31219 else {
31220 this._calculateSpringForces();
31221 }
31222 }
31223 }
31224 };
31225
31226
31227 /**
31228 * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
31229 * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
31230 * This function joins the datanodes and invisible (called support) nodes into one object.
31231 * We do this so we do not contaminate this.nodes with the support nodes.
31232 *
31233 * @private
31234 */
31235 exports._updateCalculationNodes = function () {
31236 if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
31237 this.calculationNodes = {};
31238 this.calculationNodeIndices = [];
31239
31240 for (var nodeId in this.nodes) {
31241 if (this.nodes.hasOwnProperty(nodeId)) {
31242 this.calculationNodes[nodeId] = this.nodes[nodeId];
31243 }
31244 }
31245 var supportNodes = this.sectors['support']['nodes'];
31246 for (var supportNodeId in supportNodes) {
31247 if (supportNodes.hasOwnProperty(supportNodeId)) {
31248 if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
31249 this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
31250 }
31251 else {
31252 supportNodes[supportNodeId]._setForce(0, 0);
31253 }
31254 }
31255 }
31256
31257 for (var idx in this.calculationNodes) {
31258 if (this.calculationNodes.hasOwnProperty(idx)) {
31259 this.calculationNodeIndices.push(idx);
31260 }
31261 }
31262 }
31263 else {
31264 this.calculationNodes = this.nodes;
31265 this.calculationNodeIndices = this.nodeIndices;
31266 }
31267 };
31268
31269
31270 /**
31271 * this function applies the central gravity effect to keep groups from floating off
31272 *
31273 * @private
31274 */
31275 exports._calculateGravitationalForces = function () {
31276 var dx, dy, distance, node, i;
31277 var nodes = this.calculationNodes;
31278 var gravity = this.constants.physics.centralGravity;
31279 var gravityForce = 0;
31280
31281 for (i = 0; i < this.calculationNodeIndices.length; i++) {
31282 node = nodes[this.calculationNodeIndices[i]];
31283 node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
31284 // gravity does not apply when we are in a pocket sector
31285 if (this._sector() == "default" && gravity != 0) {
31286 dx = -node.x;
31287 dy = -node.y;
31288 distance = Math.sqrt(dx * dx + dy * dy);
31289
31290 gravityForce = (distance == 0) ? 0 : (gravity / distance);
31291 node.fx = dx * gravityForce;
31292 node.fy = dy * gravityForce;
31293 }
31294 else {
31295 node.fx = 0;
31296 node.fy = 0;
31297 }
31298 }
31299 };
31300
31301
31302
31303
31304 /**
31305 * this function calculates the effects of the springs in the case of unsmooth curves.
31306 *
31307 * @private
31308 */
31309 exports._calculateSpringForces = function () {
31310 var edgeLength, edge, edgeId;
31311 var dx, dy, fx, fy, springForce, distance;
31312 var edges = this.edges;
31313
31314 // forces caused by the edges, modelled as springs
31315 for (edgeId in edges) {
31316 if (edges.hasOwnProperty(edgeId)) {
31317 edge = edges[edgeId];
31318 if (edge.connected) {
31319 // only calculate forces if nodes are in the same sector
31320 if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
31321 edgeLength = edge.physics.springLength;
31322 // this implies that the edges between big clusters are longer
31323 edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
31324
31325 dx = (edge.from.x - edge.to.x);
31326 dy = (edge.from.y - edge.to.y);
31327 distance = Math.sqrt(dx * dx + dy * dy);
31328
31329 if (distance == 0) {
31330 distance = 0.01;
31331 }
31332
31333 // the 1/distance is so the fx and fy can be calculated without sine or cosine.
31334 springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
31335
31336 fx = dx * springForce;
31337 fy = dy * springForce;
31338
31339 edge.from.fx += fx;
31340 edge.from.fy += fy;
31341 edge.to.fx -= fx;
31342 edge.to.fy -= fy;
31343 }
31344 }
31345 }
31346 }
31347 };
31348
31349
31350
31351
31352 /**
31353 * This function calculates the springforces on the nodes, accounting for the support nodes.
31354 *
31355 * @private
31356 */
31357 exports._calculateSpringForcesWithSupport = function () {
31358 var edgeLength, edge, edgeId, combinedClusterSize;
31359 var edges = this.edges;
31360
31361 // forces caused by the edges, modelled as springs
31362 for (edgeId in edges) {
31363 if (edges.hasOwnProperty(edgeId)) {
31364 edge = edges[edgeId];
31365 if (edge.connected) {
31366 // only calculate forces if nodes are in the same sector
31367 if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
31368 if (edge.via != null) {
31369 var node1 = edge.to;
31370 var node2 = edge.via;
31371 var node3 = edge.from;
31372
31373 edgeLength = edge.physics.springLength;
31374
31375 combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
31376
31377 // this implies that the edges between big clusters are longer
31378 edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
31379 this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
31380 this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
31381 }
31382 }
31383 }
31384 }
31385 }
31386 };
31387
31388
31389 /**
31390 * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
31391 *
31392 * @param node1
31393 * @param node2
31394 * @param edgeLength
31395 * @private
31396 */
31397 exports._calculateSpringForce = function (node1, node2, edgeLength) {
31398 var dx, dy, fx, fy, springForce, distance;
31399
31400 dx = (node1.x - node2.x);
31401 dy = (node1.y - node2.y);
31402 distance = Math.sqrt(dx * dx + dy * dy);
31403
31404 if (distance == 0) {
31405 distance = 0.01;
31406 }
31407
31408 // the 1/distance is so the fx and fy can be calculated without sine or cosine.
31409 springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
31410
31411 fx = dx * springForce;
31412 fy = dy * springForce;
31413
31414 node1.fx += fx;
31415 node1.fy += fy;
31416 node2.fx -= fx;
31417 node2.fy -= fy;
31418 };
31419
31420
31421 /**
31422 * Load the HTML for the physics config and bind it
31423 * @private
31424 */
31425 exports._loadPhysicsConfiguration = function () {
31426 if (this.physicsConfiguration === undefined) {
31427 this.backupConstants = {};
31428 util.deepExtend(this.backupConstants,this.constants);
31429
31430 var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
31431 this.physicsConfiguration = document.createElement('div');
31432 this.physicsConfiguration.className = "PhysicsConfiguration";
31433 this.physicsConfiguration.innerHTML = '' +
31434 '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
31435 '<tr>' +
31436 '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
31437 '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
31438 '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
31439 '</tr>' +
31440 '</table>' +
31441 '<table id="graph_BH_table" style="display:none">' +
31442 '<tr><td><b>Barnes Hut</b></td></tr>' +
31443 '<tr>' +
31444 '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
31445 '</tr>' +
31446 '<tr>' +
31447 '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
31448 '</tr>' +
31449 '<tr>' +
31450 '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
31451 '</tr>' +
31452 '<tr>' +
31453 '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
31454 '</tr>' +
31455 '<tr>' +
31456 '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
31457 '</tr>' +
31458 '</table>' +
31459 '<table id="graph_R_table" style="display:none">' +
31460 '<tr><td><b>Repulsion</b></td></tr>' +
31461 '<tr>' +
31462 '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
31463 '</tr>' +
31464 '<tr>' +
31465 '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
31466 '</tr>' +
31467 '<tr>' +
31468 '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
31469 '</tr>' +
31470 '<tr>' +
31471 '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
31472 '</tr>' +
31473 '<tr>' +
31474 '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
31475 '</tr>' +
31476 '</table>' +
31477 '<table id="graph_H_table" style="display:none">' +
31478 '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
31479 '<tr>' +
31480 '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
31481 '</tr>' +
31482 '<tr>' +
31483 '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
31484 '</tr>' +
31485 '<tr>' +
31486 '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
31487 '</tr>' +
31488 '<tr>' +
31489 '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
31490 '</tr>' +
31491 '<tr>' +
31492 '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
31493 '</tr>' +
31494 '<tr>' +
31495 '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
31496 '</tr>' +
31497 '<tr>' +
31498 '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
31499 '</tr>' +
31500 '<tr>' +
31501 '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
31502 '</tr>' +
31503 '</table>' +
31504 '<table><tr><td><b>Options:</b></td></tr>' +
31505 '<tr>' +
31506 '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
31507 '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
31508 '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
31509 '</tr>' +
31510 '</table>'
31511 this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
31512 this.optionsDiv = document.createElement("div");
31513 this.optionsDiv.style.fontSize = "14px";
31514 this.optionsDiv.style.fontFamily = "verdana";
31515 this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
31516
31517 var rangeElement;
31518 rangeElement = document.getElementById('graph_BH_gc');
31519 rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
31520 rangeElement = document.getElementById('graph_BH_cg');
31521 rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
31522 rangeElement = document.getElementById('graph_BH_sc');
31523 rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
31524 rangeElement = document.getElementById('graph_BH_sl');
31525 rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
31526 rangeElement = document.getElementById('graph_BH_damp');
31527 rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
31528
31529 rangeElement = document.getElementById('graph_R_nd');
31530 rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
31531 rangeElement = document.getElementById('graph_R_cg');
31532 rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
31533 rangeElement = document.getElementById('graph_R_sc');
31534 rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
31535 rangeElement = document.getElementById('graph_R_sl');
31536 rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
31537 rangeElement = document.getElementById('graph_R_damp');
31538 rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
31539
31540 rangeElement = document.getElementById('graph_H_nd');
31541 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
31542 rangeElement = document.getElementById('graph_H_cg');
31543 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
31544 rangeElement = document.getElementById('graph_H_sc');
31545 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
31546 rangeElement = document.getElementById('graph_H_sl');
31547 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
31548 rangeElement = document.getElementById('graph_H_damp');
31549 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
31550 rangeElement = document.getElementById('graph_H_direction');
31551 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
31552 rangeElement = document.getElementById('graph_H_levsep');
31553 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
31554 rangeElement = document.getElementById('graph_H_nspac');
31555 rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
31556
31557 var radioButton1 = document.getElementById("graph_physicsMethod1");
31558 var radioButton2 = document.getElementById("graph_physicsMethod2");
31559 var radioButton3 = document.getElementById("graph_physicsMethod3");
31560 radioButton2.checked = true;
31561 if (this.constants.physics.barnesHut.enabled) {
31562 radioButton1.checked = true;
31563 }
31564 if (this.constants.hierarchicalLayout.enabled) {
31565 radioButton3.checked = true;
31566 }
31567
31568 var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
31569 var graph_repositionNodes = document.getElementById("graph_repositionNodes");
31570 var graph_generateOptions = document.getElementById("graph_generateOptions");
31571
31572 graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
31573 graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
31574 graph_generateOptions.onclick = graphGenerateOptions.bind(this);
31575 if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
31576 graph_toggleSmooth.style.background = "#A4FF56";
31577 }
31578 else {
31579 graph_toggleSmooth.style.background = "#FF8532";
31580 }
31581
31582
31583 switchConfigurations.apply(this);
31584
31585 radioButton1.onchange = switchConfigurations.bind(this);
31586 radioButton2.onchange = switchConfigurations.bind(this);
31587 radioButton3.onchange = switchConfigurations.bind(this);
31588 }
31589 };
31590
31591 /**
31592 * This overwrites the this.constants.
31593 *
31594 * @param constantsVariableName
31595 * @param value
31596 * @private
31597 */
31598 exports._overWriteGraphConstants = function (constantsVariableName, value) {
31599 var nameArray = constantsVariableName.split("_");
31600 if (nameArray.length == 1) {
31601 this.constants[nameArray[0]] = value;
31602 }
31603 else if (nameArray.length == 2) {
31604 this.constants[nameArray[0]][nameArray[1]] = value;
31605 }
31606 else if (nameArray.length == 3) {
31607 this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
31608 }
31609 };
31610
31611
31612 /**
31613 * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
31614 */
31615 function graphToggleSmoothCurves () {
31616 this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
31617 var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
31618 if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
31619 else {graph_toggleSmooth.style.background = "#FF8532";}
31620
31621 this._configureSmoothCurves(false);
31622 }
31623
31624 /**
31625 * this function is used to scramble the nodes
31626 *
31627 */
31628 function graphRepositionNodes () {
31629 for (var nodeId in this.calculationNodes) {
31630 if (this.calculationNodes.hasOwnProperty(nodeId)) {
31631 this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
31632 this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
31633 }
31634 }
31635 if (this.constants.hierarchicalLayout.enabled == true) {
31636 this._setupHierarchicalLayout();
31637 showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
31638 showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity");
31639 showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant");
31640 showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength");
31641 showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping");
31642 }
31643 else {
31644 this.repositionNodes();
31645 }
31646 this.moving = true;
31647 this.start();
31648 }
31649
31650 /**
31651 * this is used to generate an options file from the playing with physics system.
31652 */
31653 function graphGenerateOptions () {
31654 var options = "No options are required, default values used.";
31655 var optionsSpecific = [];
31656 var radioButton1 = document.getElementById("graph_physicsMethod1");
31657 var radioButton2 = document.getElementById("graph_physicsMethod2");
31658 if (radioButton1.checked == true) {
31659 if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
31660 if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
31661 if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
31662 if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
31663 if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
31664 if (optionsSpecific.length != 0) {
31665 options = "var options = {";
31666 options += "physics: {barnesHut: {";
31667 for (var i = 0; i < optionsSpecific.length; i++) {
31668 options += optionsSpecific[i];
31669 if (i < optionsSpecific.length - 1) {
31670 options += ", "
31671 }
31672 }
31673 options += '}}'
31674 }
31675 if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
31676 if (optionsSpecific.length == 0) {options = "var options = {";}
31677 else {options += ", "}
31678 options += "smoothCurves: " + this.constants.smoothCurves.enabled;
31679 }
31680 if (options != "No options are required, default values used.") {
31681 options += '};'
31682 }
31683 }
31684 else if (radioButton2.checked == true) {
31685 options = "var options = {";
31686 options += "physics: {barnesHut: {enabled: false}";
31687 if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
31688 if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
31689 if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
31690 if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
31691 if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
31692 if (optionsSpecific.length != 0) {
31693 options += ", repulsion: {";
31694 for (var i = 0; i < optionsSpecific.length; i++) {
31695 options += optionsSpecific[i];
31696 if (i < optionsSpecific.length - 1) {
31697 options += ", "
31698 }
31699 }
31700 options += '}}'
31701 }
31702 if (optionsSpecific.length == 0) {options += "}"}
31703 if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
31704 options += ", smoothCurves: " + this.constants.smoothCurves;
31705 }
31706 options += '};'
31707 }
31708 else {
31709 options = "var options = {";
31710 if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
31711 if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
31712 if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
31713 if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
31714 if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
31715 if (optionsSpecific.length != 0) {
31716 options += "physics: {hierarchicalRepulsion: {";
31717 for (var i = 0; i < optionsSpecific.length; i++) {
31718 options += optionsSpecific[i];
31719 if (i < optionsSpecific.length - 1) {
31720 options += ", ";
31721 }
31722 }
31723 options += '}},';
31724 }
31725 options += 'hierarchicalLayout: {';
31726 optionsSpecific = [];
31727 if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
31728 if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
31729 if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
31730 if (optionsSpecific.length != 0) {
31731 for (var i = 0; i < optionsSpecific.length; i++) {
31732 options += optionsSpecific[i];
31733 if (i < optionsSpecific.length - 1) {
31734 options += ", "
31735 }
31736 }
31737 options += '}'
31738 }
31739 else {
31740 options += "enabled:true}";
31741 }
31742 options += '};'
31743 }
31744
31745
31746 this.optionsDiv.innerHTML = options;
31747 }
31748
31749 /**
31750 * this is used to switch between barnesHut, repulsion and hierarchical.
31751 *
31752 */
31753 function switchConfigurations () {
31754 var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
31755 var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
31756 var tableId = "graph_" + radioButton + "_table";
31757 var table = document.getElementById(tableId);
31758 table.style.display = "block";
31759 for (var i = 0; i < ids.length; i++) {
31760 if (ids[i] != tableId) {
31761 table = document.getElementById(ids[i]);
31762 table.style.display = "none";
31763 }
31764 }
31765 this._restoreNodes();
31766 if (radioButton == "R") {
31767 this.constants.hierarchicalLayout.enabled = false;
31768 this.constants.physics.hierarchicalRepulsion.enabled = false;
31769 this.constants.physics.barnesHut.enabled = false;
31770 }
31771 else if (radioButton == "H") {
31772 if (this.constants.hierarchicalLayout.enabled == false) {
31773 this.constants.hierarchicalLayout.enabled = true;
31774 this.constants.physics.hierarchicalRepulsion.enabled = true;
31775 this.constants.physics.barnesHut.enabled = false;
31776 this.constants.smoothCurves.enabled = false;
31777 this._setupHierarchicalLayout();
31778 }
31779 }
31780 else {
31781 this.constants.hierarchicalLayout.enabled = false;
31782 this.constants.physics.hierarchicalRepulsion.enabled = false;
31783 this.constants.physics.barnesHut.enabled = true;
31784 }
31785 this._loadSelectedForceSolver();
31786 var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
31787 if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
31788 else {graph_toggleSmooth.style.background = "#FF8532";}
31789 this.moving = true;
31790 this.start();
31791 }
31792
31793
31794 /**
31795 * this generates the ranges depending on the iniital values.
31796 *
31797 * @param id
31798 * @param map
31799 * @param constantsVariableName
31800 */
31801 function showValueOfRange (id,map,constantsVariableName) {
31802 var valueId = id + "_value";
31803 var rangeValue = document.getElementById(id).value;
31804
31805 if (map instanceof Array) {
31806 document.getElementById(valueId).value = map[parseInt(rangeValue)];
31807 this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
31808 }
31809 else {
31810 document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
31811 this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
31812 }
31813
31814 if (constantsVariableName == "hierarchicalLayout_direction" ||
31815 constantsVariableName == "hierarchicalLayout_levelSeparation" ||
31816 constantsVariableName == "hierarchicalLayout_nodeSpacing") {
31817 this._setupHierarchicalLayout();
31818 }
31819 this.moving = true;
31820 this.start();
31821 }
31822
31823
31824/***/ },
31825/* 61 */
31826/***/ function(module, exports, __webpack_require__) {
31827
31828 function webpackContext(req) {
31829 throw new Error("Cannot find module '" + req + "'.");
31830 }
31831 webpackContext.resolve = webpackContext;
31832 webpackContext.keys = function() { return []; };
31833 module.exports = webpackContext;
31834
31835
31836/***/ },
31837/* 62 */
31838/***/ function(module, exports, __webpack_require__) {
31839
31840 /**
31841 * Calculate the forces the nodes apply on each other based on a repulsion field.
31842 * This field is linearly approximated.
31843 *
31844 * @private
31845 */
31846 exports._calculateNodeForces = function () {
31847 var dx, dy, angle, distance, fx, fy, combinedClusterSize,
31848 repulsingForce, node1, node2, i, j;
31849
31850 var nodes = this.calculationNodes;
31851 var nodeIndices = this.calculationNodeIndices;
31852
31853 // approximation constants
31854 var a_base = -2 / 3;
31855 var b = 4 / 3;
31856
31857 // repulsing forces between nodes
31858 var nodeDistance = this.constants.physics.repulsion.nodeDistance;
31859 var minimumDistance = nodeDistance;
31860
31861 // we loop from i over all but the last entree in the array
31862 // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
31863 for (i = 0; i < nodeIndices.length - 1; i++) {
31864 node1 = nodes[nodeIndices[i]];
31865 for (j = i + 1; j < nodeIndices.length; j++) {
31866 node2 = nodes[nodeIndices[j]];
31867 combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
31868
31869 dx = node2.x - node1.x;
31870 dy = node2.y - node1.y;
31871 distance = Math.sqrt(dx * dx + dy * dy);
31872
31873 minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
31874 var a = a_base / minimumDistance;
31875 if (distance < 2 * minimumDistance) {
31876 if (distance < 0.5 * minimumDistance) {
31877 repulsingForce = 1.0;
31878 }
31879 else {
31880 repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
31881 }
31882
31883 // amplify the repulsion for clusters.
31884 repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
31885 repulsingForce = repulsingForce / distance;
31886
31887 fx = dx * repulsingForce;
31888 fy = dy * repulsingForce;
31889
31890 node1.fx -= fx;
31891 node1.fy -= fy;
31892 node2.fx += fx;
31893 node2.fy += fy;
31894 }
31895 }
31896 }
31897 };
31898
31899
31900/***/ },
31901/* 63 */
31902/***/ function(module, exports, __webpack_require__) {
31903
31904 /**
31905 * Calculate the forces the nodes apply on eachother based on a repulsion field.
31906 * This field is linearly approximated.
31907 *
31908 * @private
31909 */
31910 exports._calculateNodeForces = function () {
31911 var dx, dy, distance, fx, fy,
31912 repulsingForce, node1, node2, i, j;
31913
31914 var nodes = this.calculationNodes;
31915 var nodeIndices = this.calculationNodeIndices;
31916
31917 // repulsing forces between nodes
31918 var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
31919
31920 // we loop from i over all but the last entree in the array
31921 // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
31922 for (i = 0; i < nodeIndices.length - 1; i++) {
31923 node1 = nodes[nodeIndices[i]];
31924 for (j = i + 1; j < nodeIndices.length; j++) {
31925 node2 = nodes[nodeIndices[j]];
31926
31927 // nodes only affect nodes on their level
31928 if (node1.level == node2.level) {
31929
31930 dx = node2.x - node1.x;
31931 dy = node2.y - node1.y;
31932 distance = Math.sqrt(dx * dx + dy * dy);
31933
31934
31935 var steepness = 0.05;
31936 if (distance < nodeDistance) {
31937 repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2);
31938 }
31939 else {
31940 repulsingForce = 0;
31941 }
31942 // normalize force with
31943 if (distance == 0) {
31944 distance = 0.01;
31945 }
31946 else {
31947 repulsingForce = repulsingForce / distance;
31948 }
31949 fx = dx * repulsingForce;
31950 fy = dy * repulsingForce;
31951
31952 node1.fx -= fx;
31953 node1.fy -= fy;
31954 node2.fx += fx;
31955 node2.fy += fy;
31956 }
31957 }
31958 }
31959 };
31960
31961
31962 /**
31963 * this function calculates the effects of the springs in the case of unsmooth curves.
31964 *
31965 * @private
31966 */
31967 exports._calculateHierarchicalSpringForces = function () {
31968 var edgeLength, edge, edgeId;
31969 var dx, dy, fx, fy, springForce, distance;
31970 var edges = this.edges;
31971
31972 var nodes = this.calculationNodes;
31973 var nodeIndices = this.calculationNodeIndices;
31974
31975
31976 for (var i = 0; i < nodeIndices.length; i++) {
31977 var node1 = nodes[nodeIndices[i]];
31978 node1.springFx = 0;
31979 node1.springFy = 0;
31980 }
31981
31982
31983 // forces caused by the edges, modelled as springs
31984 for (edgeId in edges) {
31985 if (edges.hasOwnProperty(edgeId)) {
31986 edge = edges[edgeId];
31987 if (edge.connected) {
31988 // only calculate forces if nodes are in the same sector
31989 if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
31990 edgeLength = edge.physics.springLength;
31991 // this implies that the edges between big clusters are longer
31992 edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
31993
31994 dx = (edge.from.x - edge.to.x);
31995 dy = (edge.from.y - edge.to.y);
31996 distance = Math.sqrt(dx * dx + dy * dy);
31997
31998 if (distance == 0) {
31999 distance = 0.01;
32000 }
32001
32002 // the 1/distance is so the fx and fy can be calculated without sine or cosine.
32003 springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
32004
32005 fx = dx * springForce;
32006 fy = dy * springForce;
32007
32008
32009
32010 if (edge.to.level != edge.from.level) {
32011 edge.to.springFx -= fx;
32012 edge.to.springFy -= fy;
32013 edge.from.springFx += fx;
32014 edge.from.springFy += fy;
32015 }
32016 else {
32017 var factor = 0.5;
32018 edge.to.fx -= factor*fx;
32019 edge.to.fy -= factor*fy;
32020 edge.from.fx += factor*fx;
32021 edge.from.fy += factor*fy;
32022 }
32023 }
32024 }
32025 }
32026 }
32027
32028 // normalize spring forces
32029 var springForce = 1;
32030 var springFx, springFy;
32031 for (i = 0; i < nodeIndices.length; i++) {
32032 var node = nodes[nodeIndices[i]];
32033 springFx = Math.min(springForce,Math.max(-springForce,node.springFx));
32034 springFy = Math.min(springForce,Math.max(-springForce,node.springFy));
32035
32036 node.fx += springFx;
32037 node.fy += springFy;
32038 }
32039
32040 // retain energy balance
32041 var totalFx = 0;
32042 var totalFy = 0;
32043 for (i = 0; i < nodeIndices.length; i++) {
32044 var node = nodes[nodeIndices[i]];
32045 totalFx += node.fx;
32046 totalFy += node.fy;
32047 }
32048 var correctionFx = totalFx / nodeIndices.length;
32049 var correctionFy = totalFy / nodeIndices.length;
32050
32051 for (i = 0; i < nodeIndices.length; i++) {
32052 var node = nodes[nodeIndices[i]];
32053 node.fx -= correctionFx;
32054 node.fy -= correctionFy;
32055 }
32056
32057 };
32058
32059/***/ },
32060/* 64 */
32061/***/ function(module, exports, __webpack_require__) {
32062
32063 /**
32064 * This function calculates the forces the nodes apply on eachother based on a gravitational model.
32065 * The Barnes Hut method is used to speed up this N-body simulation.
32066 *
32067 * @private
32068 */
32069 exports._calculateNodeForces = function() {
32070 if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
32071 var node;
32072 var nodes = this.calculationNodes;
32073 var nodeIndices = this.calculationNodeIndices;
32074 var nodeCount = nodeIndices.length;
32075
32076 this._formBarnesHutTree(nodes,nodeIndices);
32077
32078 var barnesHutTree = this.barnesHutTree;
32079
32080 // place the nodes one by one recursively
32081 for (var i = 0; i < nodeCount; i++) {
32082 node = nodes[nodeIndices[i]];
32083 if (node.options.mass > 0) {
32084 // starting with root is irrelevant, it never passes the BarnesHut condition
32085 this._getForceContribution(barnesHutTree.root.children.NW,node);
32086 this._getForceContribution(barnesHutTree.root.children.NE,node);
32087 this._getForceContribution(barnesHutTree.root.children.SW,node);
32088 this._getForceContribution(barnesHutTree.root.children.SE,node);
32089 }
32090 }
32091 }
32092 };
32093
32094
32095 /**
32096 * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
32097 * If a region contains a single node, we check if it is not itself, then we apply the force.
32098 *
32099 * @param parentBranch
32100 * @param node
32101 * @private
32102 */
32103 exports._getForceContribution = function(parentBranch,node) {
32104 // we get no force contribution from an empty region
32105 if (parentBranch.childrenCount > 0) {
32106 var dx,dy,distance;
32107
32108 // get the distance from the center of mass to the node.
32109 dx = parentBranch.centerOfMass.x - node.x;
32110 dy = parentBranch.centerOfMass.y - node.y;
32111 distance = Math.sqrt(dx * dx + dy * dy);
32112
32113 // BarnesHut condition
32114 // original condition : s/d < theta = passed === d/s > 1/theta = passed
32115 // calcSize = 1/s --> d * 1/s > 1/theta = passed
32116 if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
32117 // duplicate code to reduce function calls to speed up program
32118 if (distance == 0) {
32119 distance = 0.1*Math.random();
32120 dx = distance;
32121 }
32122 var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
32123 var fx = dx * gravityForce;
32124 var fy = dy * gravityForce;
32125 node.fx += fx;
32126 node.fy += fy;
32127 }
32128 else {
32129 // Did not pass the condition, go into children if available
32130 if (parentBranch.childrenCount == 4) {
32131 this._getForceContribution(parentBranch.children.NW,node);
32132 this._getForceContribution(parentBranch.children.NE,node);
32133 this._getForceContribution(parentBranch.children.SW,node);
32134 this._getForceContribution(parentBranch.children.SE,node);
32135 }
32136 else { // parentBranch must have only one node, if it was empty we wouldnt be here
32137 if (parentBranch.children.data.id != node.id) { // if it is not self
32138 // duplicate code to reduce function calls to speed up program
32139 if (distance == 0) {
32140 distance = 0.5*Math.random();
32141 dx = distance;
32142 }
32143 var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
32144 var fx = dx * gravityForce;
32145 var fy = dy * gravityForce;
32146 node.fx += fx;
32147 node.fy += fy;
32148 }
32149 }
32150 }
32151 }
32152 };
32153
32154 /**
32155 * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
32156 *
32157 * @param nodes
32158 * @param nodeIndices
32159 * @private
32160 */
32161 exports._formBarnesHutTree = function(nodes,nodeIndices) {
32162 var node;
32163 var nodeCount = nodeIndices.length;
32164
32165 var minX = Number.MAX_VALUE,
32166 minY = Number.MAX_VALUE,
32167 maxX =-Number.MAX_VALUE,
32168 maxY =-Number.MAX_VALUE;
32169
32170 // get the range of the nodes
32171 for (var i = 0; i < nodeCount; i++) {
32172 var x = nodes[nodeIndices[i]].x;
32173 var y = nodes[nodeIndices[i]].y;
32174 if (nodes[nodeIndices[i]].options.mass > 0) {
32175 if (x < minX) { minX = x; }
32176 if (x > maxX) { maxX = x; }
32177 if (y < minY) { minY = y; }
32178 if (y > maxY) { maxY = y; }
32179 }
32180 }
32181 // make the range a square
32182 var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
32183 if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
32184 else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
32185
32186
32187 var minimumTreeSize = 1e-5;
32188 var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
32189 var halfRootSize = 0.5 * rootSize;
32190 var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
32191
32192 // construct the barnesHutTree
32193 var barnesHutTree = {
32194 root:{
32195 centerOfMass: {x:0, y:0},
32196 mass:0,
32197 range: {
32198 minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
32199 minY: centerY-halfRootSize,maxY:centerY+halfRootSize
32200 },
32201 size: rootSize,
32202 calcSize: 1 / rootSize,
32203 children: { data:null},
32204 maxWidth: 0,
32205 level: 0,
32206 childrenCount: 4
32207 }
32208 };
32209 this._splitBranch(barnesHutTree.root);
32210
32211 // place the nodes one by one recursively
32212 for (i = 0; i < nodeCount; i++) {
32213 node = nodes[nodeIndices[i]];
32214 if (node.options.mass > 0) {
32215 this._placeInTree(barnesHutTree.root,node);
32216 }
32217 }
32218
32219 // make global
32220 this.barnesHutTree = barnesHutTree
32221 };
32222
32223
32224 /**
32225 * this updates the mass of a branch. this is increased by adding a node.
32226 *
32227 * @param parentBranch
32228 * @param node
32229 * @private
32230 */
32231 exports._updateBranchMass = function(parentBranch, node) {
32232 var totalMass = parentBranch.mass + node.options.mass;
32233 var totalMassInv = 1/totalMass;
32234
32235 parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
32236 parentBranch.centerOfMass.x *= totalMassInv;
32237
32238 parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
32239 parentBranch.centerOfMass.y *= totalMassInv;
32240
32241 parentBranch.mass = totalMass;
32242 var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
32243 parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
32244
32245 };
32246
32247
32248 /**
32249 * determine in which branch the node will be placed.
32250 *
32251 * @param parentBranch
32252 * @param node
32253 * @param skipMassUpdate
32254 * @private
32255 */
32256 exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
32257 if (skipMassUpdate != true || skipMassUpdate === undefined) {
32258 // update the mass of the branch.
32259 this._updateBranchMass(parentBranch,node);
32260 }
32261
32262 if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
32263 if (parentBranch.children.NW.range.maxY > node.y) { // in NW
32264 this._placeInRegion(parentBranch,node,"NW");
32265 }
32266 else { // in SW
32267 this._placeInRegion(parentBranch,node,"SW");
32268 }
32269 }
32270 else { // in NE or SE
32271 if (parentBranch.children.NW.range.maxY > node.y) { // in NE
32272 this._placeInRegion(parentBranch,node,"NE");
32273 }
32274 else { // in SE
32275 this._placeInRegion(parentBranch,node,"SE");
32276 }
32277 }
32278 };
32279
32280
32281 /**
32282 * actually place the node in a region (or branch)
32283 *
32284 * @param parentBranch
32285 * @param node
32286 * @param region
32287 * @private
32288 */
32289 exports._placeInRegion = function(parentBranch,node,region) {
32290 switch (parentBranch.children[region].childrenCount) {
32291 case 0: // place node here
32292 parentBranch.children[region].children.data = node;
32293 parentBranch.children[region].childrenCount = 1;
32294 this._updateBranchMass(parentBranch.children[region],node);
32295 break;
32296 case 1: // convert into children
32297 // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
32298 // we move one node a pixel and we do not put it in the tree.
32299 if (parentBranch.children[region].children.data.x == node.x &&
32300 parentBranch.children[region].children.data.y == node.y) {
32301 node.x += Math.random();
32302 node.y += Math.random();
32303 }
32304 else {
32305 this._splitBranch(parentBranch.children[region]);
32306 this._placeInTree(parentBranch.children[region],node);
32307 }
32308 break;
32309 case 4: // place in branch
32310 this._placeInTree(parentBranch.children[region],node);
32311 break;
32312 }
32313 };
32314
32315
32316 /**
32317 * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
32318 * after the split is complete.
32319 *
32320 * @param parentBranch
32321 * @private
32322 */
32323 exports._splitBranch = function(parentBranch) {
32324 // if the branch is shaded with a node, replace the node in the new subset.
32325 var containedNode = null;
32326 if (parentBranch.childrenCount == 1) {
32327 containedNode = parentBranch.children.data;
32328 parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
32329 }
32330 parentBranch.childrenCount = 4;
32331 parentBranch.children.data = null;
32332 this._insertRegion(parentBranch,"NW");
32333 this._insertRegion(parentBranch,"NE");
32334 this._insertRegion(parentBranch,"SW");
32335 this._insertRegion(parentBranch,"SE");
32336
32337 if (containedNode != null) {
32338 this._placeInTree(parentBranch,containedNode);
32339 }
32340 };
32341
32342
32343 /**
32344 * This function subdivides the region into four new segments.
32345 * Specifically, this inserts a single new segment.
32346 * It fills the children section of the parentBranch
32347 *
32348 * @param parentBranch
32349 * @param region
32350 * @param parentRange
32351 * @private
32352 */
32353 exports._insertRegion = function(parentBranch, region) {
32354 var minX,maxX,minY,maxY;
32355 var childSize = 0.5 * parentBranch.size;
32356 switch (region) {
32357 case "NW":
32358 minX = parentBranch.range.minX;
32359 maxX = parentBranch.range.minX + childSize;
32360 minY = parentBranch.range.minY;
32361 maxY = parentBranch.range.minY + childSize;
32362 break;
32363 case "NE":
32364 minX = parentBranch.range.minX + childSize;
32365 maxX = parentBranch.range.maxX;
32366 minY = parentBranch.range.minY;
32367 maxY = parentBranch.range.minY + childSize;
32368 break;
32369 case "SW":
32370 minX = parentBranch.range.minX;
32371 maxX = parentBranch.range.minX + childSize;
32372 minY = parentBranch.range.minY + childSize;
32373 maxY = parentBranch.range.maxY;
32374 break;
32375 case "SE":
32376 minX = parentBranch.range.minX + childSize;
32377 maxX = parentBranch.range.maxX;
32378 minY = parentBranch.range.minY + childSize;
32379 maxY = parentBranch.range.maxY;
32380 break;
32381 }
32382
32383
32384 parentBranch.children[region] = {
32385 centerOfMass:{x:0,y:0},
32386 mass:0,
32387 range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
32388 size: 0.5 * parentBranch.size,
32389 calcSize: 2 * parentBranch.calcSize,
32390 children: {data:null},
32391 maxWidth: 0,
32392 level: parentBranch.level+1,
32393 childrenCount: 0
32394 };
32395 };
32396
32397
32398 /**
32399 * This function is for debugging purposed, it draws the tree.
32400 *
32401 * @param ctx
32402 * @param color
32403 * @private
32404 */
32405 exports._drawTree = function(ctx,color) {
32406 if (this.barnesHutTree !== undefined) {
32407
32408 ctx.lineWidth = 1;
32409
32410 this._drawBranch(this.barnesHutTree.root,ctx,color);
32411 }
32412 };
32413
32414
32415 /**
32416 * This function is for debugging purposes. It draws the branches recursively.
32417 *
32418 * @param branch
32419 * @param ctx
32420 * @param color
32421 * @private
32422 */
32423 exports._drawBranch = function(branch,ctx,color) {
32424 if (color === undefined) {
32425 color = "#FF0000";
32426 }
32427
32428 if (branch.childrenCount == 4) {
32429 this._drawBranch(branch.children.NW,ctx);
32430 this._drawBranch(branch.children.NE,ctx);
32431 this._drawBranch(branch.children.SE,ctx);
32432 this._drawBranch(branch.children.SW,ctx);
32433 }
32434 ctx.strokeStyle = color;
32435 ctx.beginPath();
32436 ctx.moveTo(branch.range.minX,branch.range.minY);
32437 ctx.lineTo(branch.range.maxX,branch.range.minY);
32438 ctx.stroke();
32439
32440 ctx.beginPath();
32441 ctx.moveTo(branch.range.maxX,branch.range.minY);
32442 ctx.lineTo(branch.range.maxX,branch.range.maxY);
32443 ctx.stroke();
32444
32445 ctx.beginPath();
32446 ctx.moveTo(branch.range.maxX,branch.range.maxY);
32447 ctx.lineTo(branch.range.minX,branch.range.maxY);
32448 ctx.stroke();
32449
32450 ctx.beginPath();
32451 ctx.moveTo(branch.range.minX,branch.range.maxY);
32452 ctx.lineTo(branch.range.minX,branch.range.minY);
32453 ctx.stroke();
32454
32455 /*
32456 if (branch.mass > 0) {
32457 ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
32458 ctx.stroke();
32459 }
32460 */
32461 };
32462
32463
32464/***/ },
32465/* 65 */
32466/***/ function(module, exports, __webpack_require__) {
32467
32468 module.exports = function(module) {
32469 if(!module.webpackPolyfill) {
32470 module.deprecate = function() {};
32471 module.paths = [];
32472 // module.parent = undefined by default
32473 module.children = [];
32474 module.webpackPolyfill = 1;
32475 }
32476 return module;
32477 }
32478
32479
32480/***/ }
32481/******/ ])
32482});