vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

519 lines
14 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * @constructor Range
  3. * A Range controls a numeric range with a start and end value.
  4. * The Range adjusts the range based on mouse events or programmatic changes,
  5. * and triggers events when the range is changing or has been changed.
  6. * @param {RootPanel} root Root panel, used to subscribe to events
  7. * @param {Panel} parent Parent panel, used to attach to the DOM
  8. * @param {Object} [options] See description at Range.setOptions
  9. */
  10. function Range(root, parent, options) {
  11. this.id = util.randomUUID();
  12. this.start = null; // Number
  13. this.end = null; // Number
  14. this.root = root;
  15. this.parent = parent;
  16. this.options = options || {};
  17. // drag listeners for dragging
  18. this.root.on('dragstart', this._onDragStart.bind(this));
  19. this.root.on('drag', this._onDrag.bind(this));
  20. this.root.on('dragend', this._onDragEnd.bind(this));
  21. // ignore dragging when holding
  22. this.root.on('hold', this._onHold.bind(this));
  23. // mouse wheel for zooming
  24. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  25. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  26. // pinch to zoom
  27. this.root.on('touch', this._onTouch.bind(this));
  28. this.root.on('pinch', this._onPinch.bind(this));
  29. this.setOptions(options);
  30. }
  31. // turn Range into an event emitter
  32. Emitter(Range.prototype);
  33. /**
  34. * Set options for the range controller
  35. * @param {Object} options Available options:
  36. * {Number} min Minimum value for start
  37. * {Number} max Maximum value for end
  38. * {Number} zoomMin Set a minimum value for
  39. * (end - start).
  40. * {Number} zoomMax Set a maximum value for
  41. * (end - start).
  42. */
  43. Range.prototype.setOptions = function (options) {
  44. util.extend(this.options, options);
  45. // re-apply range with new limitations
  46. if (this.start !== null && this.end !== null) {
  47. this.setRange(this.start, this.end);
  48. }
  49. };
  50. /**
  51. * Test whether direction has a valid value
  52. * @param {String} direction 'horizontal' or 'vertical'
  53. */
  54. function validateDirection (direction) {
  55. if (direction != 'horizontal' && direction != 'vertical') {
  56. throw new TypeError('Unknown direction "' + direction + '". ' +
  57. 'Choose "horizontal" or "vertical".');
  58. }
  59. }
  60. /**
  61. * Set a new start and end range
  62. * @param {Number} [start]
  63. * @param {Number} [end]
  64. */
  65. Range.prototype.setRange = function(start, end) {
  66. var changed = this._applyRange(start, end);
  67. if (changed) {
  68. var params = {
  69. start: new Date(this.start),
  70. end: new Date(this.end)
  71. };
  72. this.emit('rangechange', params);
  73. this.emit('rangechanged', params);
  74. }
  75. };
  76. /**
  77. * Set a new start and end range. This method is the same as setRange, but
  78. * does not trigger a range change and range changed event, and it returns
  79. * true when the range is changed
  80. * @param {Number} [start]
  81. * @param {Number} [end]
  82. * @return {Boolean} changed
  83. * @private
  84. */
  85. Range.prototype._applyRange = function(start, end) {
  86. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  87. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  88. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  89. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  90. diff;
  91. // check for valid number
  92. if (isNaN(newStart) || newStart === null) {
  93. throw new Error('Invalid start "' + start + '"');
  94. }
  95. if (isNaN(newEnd) || newEnd === null) {
  96. throw new Error('Invalid end "' + end + '"');
  97. }
  98. // prevent start < end
  99. if (newEnd < newStart) {
  100. newEnd = newStart;
  101. }
  102. // prevent start < min
  103. if (min !== null) {
  104. if (newStart < min) {
  105. diff = (min - newStart);
  106. newStart += diff;
  107. newEnd += diff;
  108. // prevent end > max
  109. if (max != null) {
  110. if (newEnd > max) {
  111. newEnd = max;
  112. }
  113. }
  114. }
  115. }
  116. // prevent end > max
  117. if (max !== null) {
  118. if (newEnd > max) {
  119. diff = (newEnd - max);
  120. newStart -= diff;
  121. newEnd -= diff;
  122. // prevent start < min
  123. if (min != null) {
  124. if (newStart < min) {
  125. newStart = min;
  126. }
  127. }
  128. }
  129. }
  130. // prevent (end-start) < zoomMin
  131. if (this.options.zoomMin !== null) {
  132. var zoomMin = parseFloat(this.options.zoomMin);
  133. if (zoomMin < 0) {
  134. zoomMin = 0;
  135. }
  136. if ((newEnd - newStart) < zoomMin) {
  137. if ((this.end - this.start) === zoomMin) {
  138. // ignore this action, we are already zoomed to the minimum
  139. newStart = this.start;
  140. newEnd = this.end;
  141. }
  142. else {
  143. // zoom to the minimum
  144. diff = (zoomMin - (newEnd - newStart));
  145. newStart -= diff / 2;
  146. newEnd += diff / 2;
  147. }
  148. }
  149. }
  150. // prevent (end-start) > zoomMax
  151. if (this.options.zoomMax !== null) {
  152. var zoomMax = parseFloat(this.options.zoomMax);
  153. if (zoomMax < 0) {
  154. zoomMax = 0;
  155. }
  156. if ((newEnd - newStart) > zoomMax) {
  157. if ((this.end - this.start) === zoomMax) {
  158. // ignore this action, we are already zoomed to the maximum
  159. newStart = this.start;
  160. newEnd = this.end;
  161. }
  162. else {
  163. // zoom to the maximum
  164. diff = ((newEnd - newStart) - zoomMax);
  165. newStart += diff / 2;
  166. newEnd -= diff / 2;
  167. }
  168. }
  169. }
  170. var changed = (this.start != newStart || this.end != newEnd);
  171. this.start = newStart;
  172. this.end = newEnd;
  173. return changed;
  174. };
  175. /**
  176. * Retrieve the current range.
  177. * @return {Object} An object with start and end properties
  178. */
  179. Range.prototype.getRange = function() {
  180. return {
  181. start: this.start,
  182. end: this.end
  183. };
  184. };
  185. /**
  186. * Calculate the conversion offset and scale for current range, based on
  187. * the provided width
  188. * @param {Number} width
  189. * @returns {{offset: number, scale: number}} conversion
  190. */
  191. Range.prototype.conversion = function (width) {
  192. return Range.conversion(this.start, this.end, width);
  193. };
  194. /**
  195. * Static method to calculate the conversion offset and scale for a range,
  196. * based on the provided start, end, and width
  197. * @param {Number} start
  198. * @param {Number} end
  199. * @param {Number} width
  200. * @returns {{offset: number, scale: number}} conversion
  201. */
  202. Range.conversion = function (start, end, width) {
  203. if (width != 0 && (end - start != 0)) {
  204. return {
  205. offset: start,
  206. scale: width / (end - start)
  207. }
  208. }
  209. else {
  210. return {
  211. offset: 0,
  212. scale: 1
  213. };
  214. }
  215. };
  216. // global (private) object to store drag params
  217. var touchParams = {};
  218. /**
  219. * Start dragging horizontally or vertically
  220. * @param {Event} event
  221. * @private
  222. */
  223. Range.prototype._onDragStart = function(event) {
  224. // refuse to drag when we where pinching to prevent the timeline make a jump
  225. // when releasing the fingers in opposite order from the touch screen
  226. if (touchParams.ignore) return;
  227. // TODO: reckon with option movable
  228. touchParams.start = this.start;
  229. touchParams.end = this.end;
  230. var frame = this.parent.frame;
  231. if (frame) {
  232. frame.style.cursor = 'move';
  233. }
  234. };
  235. /**
  236. * Perform dragging operating.
  237. * @param {Event} event
  238. * @private
  239. */
  240. Range.prototype._onDrag = function (event) {
  241. var direction = this.options.direction;
  242. validateDirection(direction);
  243. // TODO: reckon with option movable
  244. // refuse to drag when we where pinching to prevent the timeline make a jump
  245. // when releasing the fingers in opposite order from the touch screen
  246. if (touchParams.ignore) return;
  247. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  248. interval = (touchParams.end - touchParams.start),
  249. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  250. diffRange = -delta / width * interval;
  251. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  252. this.emit('rangechange', {
  253. start: new Date(this.start),
  254. end: new Date(this.end)
  255. });
  256. };
  257. /**
  258. * Stop dragging operating.
  259. * @param {event} event
  260. * @private
  261. */
  262. Range.prototype._onDragEnd = function (event) {
  263. // refuse to drag when we where pinching to prevent the timeline make a jump
  264. // when releasing the fingers in opposite order from the touch screen
  265. if (touchParams.ignore) return;
  266. // TODO: reckon with option movable
  267. if (this.parent.frame) {
  268. this.parent.frame.style.cursor = 'auto';
  269. }
  270. // fire a rangechanged event
  271. this.emit('rangechanged', {
  272. start: new Date(this.start),
  273. end: new Date(this.end)
  274. });
  275. };
  276. /**
  277. * Event handler for mouse wheel event, used to zoom
  278. * Code from http://adomas.org/javascript-mouse-wheel/
  279. * @param {Event} event
  280. * @private
  281. */
  282. Range.prototype._onMouseWheel = function(event) {
  283. // TODO: reckon with option zoomable
  284. // retrieve delta
  285. var delta = 0;
  286. if (event.wheelDelta) { /* IE/Opera. */
  287. delta = event.wheelDelta / 120;
  288. } else if (event.detail) { /* Mozilla case. */
  289. // In Mozilla, sign of delta is different than in IE.
  290. // Also, delta is multiple of 3.
  291. delta = -event.detail / 3;
  292. }
  293. // If delta is nonzero, handle it.
  294. // Basically, delta is now positive if wheel was scrolled up,
  295. // and negative, if wheel was scrolled down.
  296. if (delta) {
  297. // perform the zoom action. Delta is normally 1 or -1
  298. // adjust a negative delta such that zooming in with delta 0.1
  299. // equals zooming out with a delta -0.1
  300. var scale;
  301. if (delta < 0) {
  302. scale = 1 - (delta / 5);
  303. }
  304. else {
  305. scale = 1 / (1 + (delta / 5)) ;
  306. }
  307. // calculate center, the date to zoom around
  308. var gesture = util.fakeGesture(this, event),
  309. pointer = getPointer(gesture.center, this.parent.frame),
  310. pointerDate = this._pointerToDate(pointer);
  311. this.zoom(scale, pointerDate);
  312. }
  313. // Prevent default actions caused by mouse wheel
  314. // (else the page and timeline both zoom and scroll)
  315. event.preventDefault();
  316. };
  317. /**
  318. * Start of a touch gesture
  319. * @private
  320. */
  321. Range.prototype._onTouch = function (event) {
  322. touchParams.start = this.start;
  323. touchParams.end = this.end;
  324. touchParams.ignore = false;
  325. touchParams.center = null;
  326. // don't move the range when dragging a selected event
  327. // TODO: it's not so neat to have to know about the state of the ItemSet
  328. var item = ItemSet.itemFromTarget(event);
  329. if (item && item.selected && this.options.editable) {
  330. touchParams.ignore = true;
  331. }
  332. };
  333. /**
  334. * On start of a hold gesture
  335. * @private
  336. */
  337. Range.prototype._onHold = function () {
  338. touchParams.ignore = true;
  339. };
  340. /**
  341. * Handle pinch event
  342. * @param {Event} event
  343. * @private
  344. */
  345. Range.prototype._onPinch = function (event) {
  346. var direction = this.options.direction;
  347. touchParams.ignore = true;
  348. // TODO: reckon with option zoomable
  349. if (event.gesture.touches.length > 1) {
  350. if (!touchParams.center) {
  351. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  352. }
  353. var scale = 1 / event.gesture.scale,
  354. initDate = this._pointerToDate(touchParams.center),
  355. center = getPointer(event.gesture.center, this.parent.frame),
  356. date = this._pointerToDate(this.parent, center),
  357. delta = date - initDate; // TODO: utilize delta
  358. // calculate new start and end
  359. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  360. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  361. // apply new range
  362. this.setRange(newStart, newEnd);
  363. }
  364. };
  365. /**
  366. * Helper function to calculate the center date for zooming
  367. * @param {{x: Number, y: Number}} pointer
  368. * @return {number} date
  369. * @private
  370. */
  371. Range.prototype._pointerToDate = function (pointer) {
  372. var conversion;
  373. var direction = this.options.direction;
  374. validateDirection(direction);
  375. if (direction == 'horizontal') {
  376. var width = this.parent.width;
  377. conversion = this.conversion(width);
  378. return pointer.x / conversion.scale + conversion.offset;
  379. }
  380. else {
  381. var height = this.parent.height;
  382. conversion = this.conversion(height);
  383. return pointer.y / conversion.scale + conversion.offset;
  384. }
  385. };
  386. /**
  387. * Get the pointer location relative to the location of the dom element
  388. * @param {{pageX: Number, pageY: Number}} touch
  389. * @param {Element} element HTML DOM element
  390. * @return {{x: Number, y: Number}} pointer
  391. * @private
  392. */
  393. function getPointer (touch, element) {
  394. return {
  395. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  396. y: touch.pageY - vis.util.getAbsoluteTop(element)
  397. };
  398. }
  399. /**
  400. * Zoom the range the given scale in or out. Start and end date will
  401. * be adjusted, and the timeline will be redrawn. You can optionally give a
  402. * date around which to zoom.
  403. * For example, try scale = 0.9 or 1.1
  404. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  405. * values below 1 will zoom in.
  406. * @param {Number} [center] Value representing a date around which will
  407. * be zoomed.
  408. */
  409. Range.prototype.zoom = function(scale, center) {
  410. // if centerDate is not provided, take it half between start Date and end Date
  411. if (center == null) {
  412. center = (this.start + this.end) / 2;
  413. }
  414. // calculate new start and end
  415. var newStart = center + (this.start - center) * scale;
  416. var newEnd = center + (this.end - center) * scale;
  417. this.setRange(newStart, newEnd);
  418. };
  419. /**
  420. * Move the range with a given delta to the left or right. Start and end
  421. * value will be adjusted. For example, try delta = 0.1 or -0.1
  422. * @param {Number} delta Moving amount. Positive value will move right,
  423. * negative value will move left
  424. */
  425. Range.prototype.move = function(delta) {
  426. // zoom start Date and end Date relative to the centerDate
  427. var diff = (this.end - this.start);
  428. // apply new values
  429. var newStart = this.start + diff * delta;
  430. var newEnd = this.end + diff * delta;
  431. // TODO: reckon with min and max range
  432. this.start = newStart;
  433. this.end = newEnd;
  434. };
  435. /**
  436. * Move the range to a new center point
  437. * @param {Number} moveTo New center point of the range
  438. */
  439. Range.prototype.moveTo = function(moveTo) {
  440. var center = (this.start + this.end) / 2;
  441. var diff = center - moveTo;
  442. // calculate new start and end
  443. var newStart = this.start - diff;
  444. var newEnd = this.end - diff;
  445. this.setRange(newStart, newEnd);
  446. };