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.

675 lines
21 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. var util = require('../util');
  2. var hammerUtil = require('../hammerUtil');
  3. var moment = require('../module/moment');
  4. var Component = require('./component/Component');
  5. var DateUtil = require('./DateUtil');
  6. /**
  7. * @constructor Range
  8. * A Range controls a numeric range with a start and end value.
  9. * The Range adjusts the range based on mouse events or programmatic changes,
  10. * and triggers events when the range is changing or has been changed.
  11. * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
  12. * @param {Object} [options] See description at Range.setOptions
  13. */
  14. function Range(body, options) {
  15. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  16. this.start = now.clone().add(-3, 'days').valueOf(); // Number
  17. this.end = now.clone().add(4, 'days').valueOf(); // Number
  18. this.body = body;
  19. this.deltaDifference = 0;
  20. this.scaleOffset = 0;
  21. this.startToFront = false;
  22. this.endToFront = true;
  23. // default options
  24. this.defaultOptions = {
  25. start: null,
  26. end: null,
  27. direction: 'horizontal', // 'horizontal' or 'vertical'
  28. moveable: true,
  29. zoomable: true,
  30. min: null,
  31. max: null,
  32. zoomMin: 10, // milliseconds
  33. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
  34. };
  35. this.options = util.extend({}, this.defaultOptions);
  36. this.props = {
  37. touch: {}
  38. };
  39. this.animateTimer = null;
  40. // drag listeners for dragging
  41. this.body.emitter.on('panstart', this._onDragStart.bind(this));
  42. this.body.emitter.on('panmove', this._onDrag.bind(this));
  43. this.body.emitter.on('panend', this._onDragEnd.bind(this));
  44. // ignore dragging when holding
  45. this.body.emitter.on('press', this._onHold.bind(this));
  46. // mouse wheel for zooming
  47. this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
  48. // pinch to zoom
  49. this.body.emitter.on('touch', this._onTouch.bind(this));
  50. this.body.emitter.on('pinch', this._onPinch.bind(this));
  51. this.setOptions(options);
  52. }
  53. Range.prototype = new Component();
  54. /**
  55. * Set options for the range controller
  56. * @param {Object} options Available options:
  57. * {Number | Date | String} start Start date for the range
  58. * {Number | Date | String} end End date for the range
  59. * {Number} min Minimum value for start
  60. * {Number} max Maximum value for end
  61. * {Number} zoomMin Set a minimum value for
  62. * (end - start).
  63. * {Number} zoomMax Set a maximum value for
  64. * (end - start).
  65. * {Boolean} moveable Enable moving of the range
  66. * by dragging. True by default
  67. * {Boolean} zoomable Enable zooming of the range
  68. * by pinching/scrolling. True by default
  69. */
  70. Range.prototype.setOptions = function (options) {
  71. if (options) {
  72. // copy the options that we know
  73. var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates'];
  74. util.selectiveExtend(fields, this.options, options);
  75. if ('start' in options || 'end' in options) {
  76. // apply a new range. both start and end are optional
  77. this.setRange(options.start, options.end);
  78. }
  79. }
  80. };
  81. /**
  82. * Test whether direction has a valid value
  83. * @param {String} direction 'horizontal' or 'vertical'
  84. */
  85. function validateDirection (direction) {
  86. if (direction != 'horizontal' && direction != 'vertical') {
  87. throw new TypeError('Unknown direction "' + direction + '". ' +
  88. 'Choose "horizontal" or "vertical".');
  89. }
  90. }
  91. /**
  92. * Set a new start and end range
  93. * @param {Date | Number | String} [start]
  94. * @param {Date | Number | String} [end]
  95. * @param {boolean | number} [animate=false] If true, the range is animated
  96. * smoothly to the new window.
  97. * If animate is a number, the
  98. * number is taken as duration
  99. * Default duration is 500 ms.
  100. *
  101. */
  102. Range.prototype.setRange = function(start, end, animate) {
  103. var _start = start != undefined ? util.convert(start, 'Date').valueOf() : null;
  104. var _end = end != undefined ? util.convert(end, 'Date').valueOf() : null;
  105. this._cancelAnimation();
  106. if (animate) {
  107. var me = this;
  108. var initStart = this.start;
  109. var initEnd = this.end;
  110. var duration = typeof animate === 'number' ? animate : 500;
  111. var initTime = new Date().valueOf();
  112. var anyChanged = false;
  113. var next = function () {
  114. if (!me.props.touch.dragging) {
  115. var now = new Date().valueOf();
  116. var time = now - initTime;
  117. var done = time > duration;
  118. var s = (done || _start === null) ? _start : util.easeInOutQuad(time, initStart, _start, duration);
  119. var e = (done || _end === null) ? _end : util.easeInOutQuad(time, initEnd, _end, duration);
  120. changed = me._applyRange(s, e);
  121. DateUtil.updateHiddenDates(me.body, me.options.hiddenDates);
  122. anyChanged = anyChanged || changed;
  123. if (changed) {
  124. me.body.emitter.emit('rangechange', {start: new Date(me.start), end: new Date(me.end)});
  125. }
  126. if (done) {
  127. if (anyChanged) {
  128. me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end)});
  129. }
  130. }
  131. else {
  132. // animate with as high as possible frame rate, leave 20 ms in between
  133. // each to prevent the browser from blocking
  134. me.animateTimer = setTimeout(next, 20);
  135. }
  136. }
  137. }
  138. return next();
  139. }
  140. else {
  141. var changed = this._applyRange(_start, _end);
  142. DateUtil.updateHiddenDates(this.body, this.options.hiddenDates);
  143. if (changed) {
  144. var params = {start: new Date(this.start), end: new Date(this.end)};
  145. this.body.emitter.emit('rangechange', params);
  146. this.body.emitter.emit('rangechanged', params);
  147. }
  148. }
  149. };
  150. /**
  151. * Stop an animation
  152. * @private
  153. */
  154. Range.prototype._cancelAnimation = function () {
  155. if (this.animateTimer) {
  156. clearTimeout(this.animateTimer);
  157. this.animateTimer = null;
  158. }
  159. };
  160. /**
  161. * Set a new start and end range. This method is the same as setRange, but
  162. * does not trigger a range change and range changed event, and it returns
  163. * true when the range is changed
  164. * @param {Number} [start]
  165. * @param {Number} [end]
  166. * @return {Boolean} changed
  167. * @private
  168. */
  169. Range.prototype._applyRange = function(start, end) {
  170. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  171. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  172. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  173. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  174. diff;
  175. // check for valid number
  176. if (isNaN(newStart) || newStart === null) {
  177. throw new Error('Invalid start "' + start + '"');
  178. }
  179. if (isNaN(newEnd) || newEnd === null) {
  180. throw new Error('Invalid end "' + end + '"');
  181. }
  182. // prevent start < end
  183. if (newEnd < newStart) {
  184. newEnd = newStart;
  185. }
  186. // prevent start < min
  187. if (min !== null) {
  188. if (newStart < min) {
  189. diff = (min - newStart);
  190. newStart += diff;
  191. newEnd += diff;
  192. // prevent end > max
  193. if (max != null) {
  194. if (newEnd > max) {
  195. newEnd = max;
  196. }
  197. }
  198. }
  199. }
  200. // prevent end > max
  201. if (max !== null) {
  202. if (newEnd > max) {
  203. diff = (newEnd - max);
  204. newStart -= diff;
  205. newEnd -= diff;
  206. // prevent start < min
  207. if (min != null) {
  208. if (newStart < min) {
  209. newStart = min;
  210. }
  211. }
  212. }
  213. }
  214. // prevent (end-start) < zoomMin
  215. if (this.options.zoomMin !== null) {
  216. var zoomMin = parseFloat(this.options.zoomMin);
  217. if (zoomMin < 0) {
  218. zoomMin = 0;
  219. }
  220. if ((newEnd - newStart) < zoomMin) {
  221. if ((this.end - this.start) === zoomMin) {
  222. // ignore this action, we are already zoomed to the minimum
  223. newStart = this.start;
  224. newEnd = this.end;
  225. }
  226. else {
  227. // zoom to the minimum
  228. diff = (zoomMin - (newEnd - newStart));
  229. newStart -= diff / 2;
  230. newEnd += diff / 2;
  231. }
  232. }
  233. }
  234. // prevent (end-start) > zoomMax
  235. if (this.options.zoomMax !== null) {
  236. var zoomMax = parseFloat(this.options.zoomMax);
  237. if (zoomMax < 0) {
  238. zoomMax = 0;
  239. }
  240. if ((newEnd - newStart) > zoomMax) {
  241. if ((this.end - this.start) === zoomMax) {
  242. // ignore this action, we are already zoomed to the maximum
  243. newStart = this.start;
  244. newEnd = this.end;
  245. }
  246. else {
  247. // zoom to the maximum
  248. diff = ((newEnd - newStart) - zoomMax);
  249. newStart += diff / 2;
  250. newEnd -= diff / 2;
  251. }
  252. }
  253. }
  254. var changed = (this.start != newStart || this.end != newEnd);
  255. // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not neccesarily of type Range)
  256. if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&
  257. !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {
  258. this.body.emitter.emit('checkRangedItems');
  259. }
  260. this.start = newStart;
  261. this.end = newEnd;
  262. return changed;
  263. };
  264. /**
  265. * Retrieve the current range.
  266. * @return {Object} An object with start and end properties
  267. */
  268. Range.prototype.getRange = function() {
  269. return {
  270. start: this.start,
  271. end: this.end
  272. };
  273. };
  274. /**
  275. * Calculate the conversion offset and scale for current range, based on
  276. * the provided width
  277. * @param {Number} width
  278. * @returns {{offset: number, scale: number}} conversion
  279. */
  280. Range.prototype.conversion = function (width, totalHidden) {
  281. return Range.conversion(this.start, this.end, width, totalHidden);
  282. };
  283. /**
  284. * Static method to calculate the conversion offset and scale for a range,
  285. * based on the provided start, end, and width
  286. * @param {Number} start
  287. * @param {Number} end
  288. * @param {Number} width
  289. * @returns {{offset: number, scale: number}} conversion
  290. */
  291. Range.conversion = function (start, end, width, totalHidden) {
  292. if (totalHidden === undefined) {
  293. totalHidden = 0;
  294. }
  295. if (width != 0 && (end - start != 0)) {
  296. return {
  297. offset: start,
  298. scale: width / (end - start - totalHidden)
  299. }
  300. }
  301. else {
  302. return {
  303. offset: 0,
  304. scale: 1
  305. };
  306. }
  307. };
  308. /**
  309. * Start dragging horizontally or vertically
  310. * @param {Event} event
  311. * @private
  312. */
  313. Range.prototype._onDragStart = function(event) {
  314. this.deltaDifference = 0;
  315. this.previousDelta = 0;
  316. // only allow dragging when configured as movable
  317. if (!this.options.moveable) return;
  318. // refuse to drag when we where pinching to prevent the timeline make a jump
  319. // when releasing the fingers in opposite order from the touch screen
  320. if (!this.props.touch.allowDragging) return;
  321. this.props.touch.start = this.start;
  322. this.props.touch.end = this.end;
  323. this.props.touch.dragging = true;
  324. if (this.body.dom.root) {
  325. this.body.dom.root.style.cursor = 'move';
  326. }
  327. };
  328. /**
  329. * Perform dragging operation
  330. * @param {Event} event
  331. * @private
  332. */
  333. Range.prototype._onDrag = function (event) {
  334. // only allow dragging when configured as movable
  335. if (!this.options.moveable) return;
  336. // TODO: this may be redundant in hammerjs2
  337. // refuse to drag when we where pinching to prevent the timeline make a jump
  338. // when releasing the fingers in opposite order from the touch screen
  339. if (!this.props.touch.allowDragging) return;
  340. var direction = this.options.direction;
  341. validateDirection(direction);
  342. var delta = (direction == 'horizontal') ? event.deltaX : event.deltaY;
  343. delta -= this.deltaDifference;
  344. var interval = (this.props.touch.end - this.props.touch.start);
  345. // normalize dragging speed if cutout is in between.
  346. var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  347. interval -= duration;
  348. var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
  349. var diffRange = -delta / width * interval;
  350. var newStart = this.props.touch.start + diffRange;
  351. var newEnd = this.props.touch.end + diffRange;
  352. // snapping times away from hidden zones
  353. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true);
  354. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true);
  355. if (safeStart != newStart || safeEnd != newEnd) {
  356. this.deltaDifference += delta;
  357. this.props.touch.start = safeStart;
  358. this.props.touch.end = safeEnd;
  359. this._onDrag(event);
  360. return;
  361. }
  362. this.previousDelta = delta;
  363. this._applyRange(newStart, newEnd);
  364. // fire a rangechange event
  365. this.body.emitter.emit('rangechange', {
  366. start: new Date(this.start),
  367. end: new Date(this.end)
  368. });
  369. };
  370. /**
  371. * Stop dragging operation
  372. * @param {event} event
  373. * @private
  374. */
  375. Range.prototype._onDragEnd = function (event) {
  376. // only allow dragging when configured as movable
  377. if (!this.options.moveable) return;
  378. // TODO: this may be redundant in hammerjs2
  379. // refuse to drag when we where pinching to prevent the timeline make a jump
  380. // when releasing the fingers in opposite order from the touch screen
  381. if (!this.props.touch.allowDragging) return;
  382. this.props.touch.dragging = false;
  383. if (this.body.dom.root) {
  384. this.body.dom.root.style.cursor = 'auto';
  385. }
  386. // fire a rangechanged event
  387. this.body.emitter.emit('rangechanged', {
  388. start: new Date(this.start),
  389. end: new Date(this.end)
  390. });
  391. };
  392. /**
  393. * Event handler for mouse wheel event, used to zoom
  394. * Code from http://adomas.org/javascript-mouse-wheel/
  395. * @param {Event} event
  396. * @private
  397. */
  398. Range.prototype._onMouseWheel = function(event) {
  399. // only allow zooming when configured as zoomable and moveable
  400. if (!(this.options.zoomable && this.options.moveable)) return;
  401. // retrieve delta
  402. var delta = 0;
  403. if (event.wheelDelta) { /* IE/Opera. */
  404. delta = event.wheelDelta / 120;
  405. } else if (event.detail) { /* Mozilla case. */
  406. // In Mozilla, sign of delta is different than in IE.
  407. // Also, delta is multiple of 3.
  408. delta = -event.detail / 3;
  409. }
  410. // If delta is nonzero, handle it.
  411. // Basically, delta is now positive if wheel was scrolled up,
  412. // and negative, if wheel was scrolled down.
  413. if (delta) {
  414. // perform the zoom action. Delta is normally 1 or -1
  415. // adjust a negative delta such that zooming in with delta 0.1
  416. // equals zooming out with a delta -0.1
  417. var scale;
  418. if (delta < 0) {
  419. scale = 1 - (delta / 5);
  420. }
  421. else {
  422. scale = 1 / (1 + (delta / 5)) ;
  423. }
  424. // calculate center, the date to zoom around
  425. var pointer = getPointer({x: event.pageX, y: event.pageY}, this.body.dom.center);
  426. var pointerDate = this._pointerToDate(pointer);
  427. this.zoom(scale, pointerDate, delta);
  428. }
  429. // Prevent default actions caused by mouse wheel
  430. // (else the page and timeline both zoom and scroll)
  431. event.preventDefault();
  432. };
  433. /**
  434. * Start of a touch gesture
  435. * @private
  436. */
  437. Range.prototype._onTouch = function (event) {
  438. this.props.touch.start = this.start;
  439. this.props.touch.end = this.end;
  440. this.props.touch.allowDragging = true;
  441. this.props.touch.center = null;
  442. this.scaleOffset = 0;
  443. this.deltaDifference = 0;
  444. };
  445. /**
  446. * On start of a hold gesture
  447. * @private
  448. */
  449. Range.prototype._onHold = function () {
  450. this.props.touch.allowDragging = false;
  451. };
  452. /**
  453. * Handle pinch event
  454. * @param {Event} event
  455. * @private
  456. */
  457. Range.prototype._onPinch = function (event) {
  458. // only allow zooming when configured as zoomable and moveable
  459. if (!(this.options.zoomable && this.options.moveable)) return;
  460. this.props.touch.allowDragging = false;
  461. if (event.touches.length > 1) {
  462. if (!this.props.touch.center) {
  463. this.props.touch.center = getPointer(event.center, this.body.dom.center);
  464. }
  465. var scale = 1 / (event.scale + this.scaleOffset);
  466. var centerDate = this._pointerToDate(this.props.touch.center);
  467. var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  468. var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate);
  469. var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
  470. // calculate new start and end
  471. var newStart = (centerDate - hiddenDurationBefore) + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale;
  472. var newEnd = (centerDate + hiddenDurationAfter) + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale;
  473. // snapping times away from hidden zones
  474. this.startToFront = 1 - scale > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  475. this.endToFront = scale - 1 > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  476. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true);
  477. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true);
  478. if (safeStart != newStart || safeEnd != newEnd) {
  479. this.props.touch.start = safeStart;
  480. this.props.touch.end = safeEnd;
  481. this.scaleOffset = 1 - event.scale;
  482. newStart = safeStart;
  483. newEnd = safeEnd;
  484. }
  485. this.setRange(newStart, newEnd);
  486. this.startToFront = false; // revert to default
  487. this.endToFront = true; // revert to default
  488. }
  489. };
  490. /**
  491. * Helper function to calculate the center date for zooming
  492. * @param {{x: Number, y: Number}} pointer
  493. * @return {number} date
  494. * @private
  495. */
  496. Range.prototype._pointerToDate = function (pointer) {
  497. var conversion;
  498. var direction = this.options.direction;
  499. validateDirection(direction);
  500. if (direction == 'horizontal') {
  501. return this.body.util.toTime(pointer.x).valueOf();
  502. }
  503. else {
  504. var height = this.body.domProps.center.height;
  505. conversion = this.conversion(height);
  506. return pointer.y / conversion.scale + conversion.offset;
  507. }
  508. };
  509. /**
  510. * Get the pointer location relative to the location of the dom element
  511. * @param {{x: Number, y: Number}} touch
  512. * @param {Element} element HTML DOM element
  513. * @return {{x: Number, y: Number}} pointer
  514. * @private
  515. */
  516. function getPointer (touch, element) {
  517. return {
  518. x: touch.x - util.getAbsoluteLeft(element),
  519. y: touch.y - util.getAbsoluteTop(element)
  520. };
  521. }
  522. /**
  523. * Zoom the range the given scale in or out. Start and end date will
  524. * be adjusted, and the timeline will be redrawn. You can optionally give a
  525. * date around which to zoom.
  526. * For example, try scale = 0.9 or 1.1
  527. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  528. * values below 1 will zoom in.
  529. * @param {Number} [center] Value representing a date around which will
  530. * be zoomed.
  531. */
  532. Range.prototype.zoom = function(scale, center, delta) {
  533. // if centerDate is not provided, take it half between start Date and end Date
  534. if (center == null) {
  535. center = (this.start + this.end) / 2;
  536. }
  537. var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);
  538. var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center);
  539. var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore;
  540. // calculate new start and end
  541. var newStart = (center-hiddenDurationBefore) + (this.start - (center-hiddenDurationBefore)) * scale;
  542. var newEnd = (center+hiddenDurationAfter) + (this.end - (center+hiddenDurationAfter)) * scale;
  543. // snapping times away from hidden zones
  544. this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  545. this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times
  546. var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true);
  547. var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true);
  548. if (safeStart != newStart || safeEnd != newEnd) {
  549. newStart = safeStart;
  550. newEnd = safeEnd;
  551. }
  552. this.setRange(newStart, newEnd);
  553. this.startToFront = false; // revert to default
  554. this.endToFront = true; // revert to default
  555. };
  556. /**
  557. * Move the range with a given delta to the left or right. Start and end
  558. * value will be adjusted. For example, try delta = 0.1 or -0.1
  559. * @param {Number} delta Moving amount. Positive value will move right,
  560. * negative value will move left
  561. */
  562. Range.prototype.move = function(delta) {
  563. // zoom start Date and end Date relative to the centerDate
  564. var diff = (this.end - this.start);
  565. // apply new values
  566. var newStart = this.start + diff * delta;
  567. var newEnd = this.end + diff * delta;
  568. // TODO: reckon with min and max range
  569. this.start = newStart;
  570. this.end = newEnd;
  571. };
  572. /**
  573. * Move the range to a new center point
  574. * @param {Number} moveTo New center point of the range
  575. */
  576. Range.prototype.moveTo = function(moveTo) {
  577. var center = (this.start + this.end) / 2;
  578. var diff = center - moveTo;
  579. // calculate new start and end
  580. var newStart = this.start - diff;
  581. var newEnd = this.end - diff;
  582. this.setRange(newStart, newEnd);
  583. };
  584. module.exports = Range;