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.

275 lines
8.7 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /**
  2. * @constructor DataStep
  3. * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
  4. * end data point. The class itself determines the best scale (step size) based on the
  5. * provided start Date, end Date, and minimumStep.
  6. *
  7. * If minimumStep is provided, the step size is chosen as close as possible
  8. * to the minimumStep but larger than minimumStep. If minimumStep is not
  9. * provided, the scale is set to 1 DAY.
  10. * The minimumStep should correspond with the onscreen size of about 6 characters
  11. *
  12. * Alternatively, you can set a scale by hand.
  13. * After creation, you can initialize the class by executing first(). Then you
  14. * can iterate from the start date to the end date via next(). You can check if
  15. * the end date is reached with the function hasNext(). After each step, you can
  16. * retrieve the current date via getCurrent().
  17. * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
  18. * days, to years.
  19. *
  20. * Version: 1.2
  21. *
  22. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  23. * or new Date(2010, 9, 21, 23, 45, 00)
  24. * @param {Date} [end] The end date
  25. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  26. */
  27. function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) {
  28. // variables
  29. this.current = 0;
  30. this.autoScale = true;
  31. this.stepIndex = 0;
  32. this.step = 1;
  33. this.scale = 1;
  34. this.marginStart;
  35. this.marginEnd;
  36. this.deadSpace = 0;
  37. this.majorSteps = [1, 2, 5, 10];
  38. this.minorSteps = [0.25, 0.5, 1, 2];
  39. this.alignZeros = alignZeros;
  40. this.setRange(start, end, minimumStep, containerHeight, customRange);
  41. }
  42. /**
  43. * Set a new range
  44. * If minimumStep is provided, the step size is chosen as close as possible
  45. * to the minimumStep but larger than minimumStep. If minimumStep is not
  46. * provided, the scale is set to 1 DAY.
  47. * The minimumStep should correspond with the onscreen size of about 6 characters
  48. * @param {Number} [start] The start date and time.
  49. * @param {Number} [end] The end date and time.
  50. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  51. */
  52. DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
  53. this._start = customRange.min === undefined ? start : customRange.min;
  54. this._end = customRange.max === undefined ? end : customRange.max;
  55. if (this._start === this._end) {
  56. this._start = customRange.min === undefined ? this._start - 0.75 : this._start;
  57. this._end = customRange.max === undefined ? this._end + 1 : this._end;;
  58. }
  59. if (this.autoScale === true) {
  60. this.setMinimumStep(minimumStep, containerHeight);
  61. }
  62. this.setFirst(customRange);
  63. };
  64. /**
  65. * Automatically determine the scale that bests fits the provided minimum step
  66. * @param {Number} [minimumStep] The minimum step size in pixels
  67. */
  68. DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
  69. // round to floor
  70. var range = this._end - this._start;
  71. var safeRange = range * 1.2;
  72. var minimumStepValue = minimumStep * (safeRange / containerHeight);
  73. var orderOfMagnitude = Math.round(Math.log(safeRange)/Math.LN10);
  74. var minorStepIdx = -1;
  75. var magnitudefactor = Math.pow(10,orderOfMagnitude);
  76. var start = 0;
  77. if (orderOfMagnitude < 0) {
  78. start = orderOfMagnitude;
  79. }
  80. var solutionFound = false;
  81. for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
  82. magnitudefactor = Math.pow(10,i);
  83. for (var j = 0; j < this.minorSteps.length; j++) {
  84. var stepSize = magnitudefactor * this.minorSteps[j];
  85. if (stepSize >= minimumStepValue) {
  86. solutionFound = true;
  87. minorStepIdx = j;
  88. break;
  89. }
  90. }
  91. if (solutionFound === true) {
  92. break;
  93. }
  94. }
  95. this.stepIndex = minorStepIdx;
  96. this.scale = magnitudefactor;
  97. this.step = magnitudefactor * this.minorSteps[minorStepIdx];
  98. };
  99. /**
  100. * Round the current date to the first minor date value
  101. * This must be executed once when the current date is set to start Date
  102. */
  103. DataStep.prototype.setFirst = function(customRange) {
  104. if (customRange === undefined) {
  105. customRange = {};
  106. }
  107. var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
  108. var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
  109. this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
  110. this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
  111. // if we need to align the zero's we need to make sure that there is a zero to use.
  112. if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) {
  113. this.marginEnd += this.marginEnd % this.step;
  114. }
  115. this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
  116. this.marginRange = this.marginEnd - this.marginStart;
  117. this.current = this.marginEnd;
  118. };
  119. DataStep.prototype.roundToMinor = function(value) {
  120. var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
  121. if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
  122. return rounded + (this.scale * this.minorSteps[this.stepIndex]);
  123. }
  124. else {
  125. return rounded;
  126. }
  127. }
  128. /**
  129. * Check if the there is a next step
  130. * @return {boolean} true if the current date has not passed the end date
  131. */
  132. DataStep.prototype.hasNext = function () {
  133. return (this.current >= this.marginStart);
  134. };
  135. /**
  136. * Do the next step
  137. */
  138. DataStep.prototype.next = function() {
  139. var prev = this.current;
  140. this.current -= this.step;
  141. // safety mechanism: if current time is still unchanged, move to the end
  142. if (this.current === prev) {
  143. this.current = this._end;
  144. }
  145. };
  146. /**
  147. * Do the next step
  148. */
  149. DataStep.prototype.previous = function() {
  150. this.current += this.step;
  151. this.marginEnd += this.step;
  152. this.marginRange = this.marginEnd - this.marginStart;
  153. };
  154. /**
  155. * Get the current datetime
  156. * @return {String} current The current date
  157. */
  158. DataStep.prototype.getCurrent = function(decimals) {
  159. // prevent round-off errors when close to zero
  160. var current = (Math.abs(this.current) < this.step / 2) ? 0 : this.current;
  161. var toPrecision = '' + Number(current).toPrecision(5);
  162. // If decimals is specified, then limit or extend the string as required
  163. if(decimals !== undefined && !isNaN(Number(decimals))) {
  164. // If string includes exponent, then we need to add it to the end
  165. var exp = "";
  166. var index = toPrecision.indexOf("e");
  167. if(index != -1) {
  168. // Get the exponent
  169. exp = toPrecision.slice(index);
  170. // Remove the exponent in case we need to zero-extend
  171. toPrecision = toPrecision.slice(0, index);
  172. }
  173. index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf("."));
  174. if(index === -1) {
  175. // No decimal found - if we want decimals, then we need to add it
  176. if(decimals !== 0) {
  177. toPrecision += '.';
  178. }
  179. // Calculate how long the string should be
  180. index = toPrecision.length + decimals;
  181. }
  182. else if(decimals !== 0) {
  183. // Calculate how long the string should be - accounting for the decimal place
  184. index += decimals + 1;
  185. }
  186. if(index > toPrecision.length) {
  187. // We need to add zeros!
  188. for(var cnt = index - toPrecision.length; cnt > 0; cnt--) {
  189. toPrecision += '0';
  190. }
  191. }
  192. else {
  193. // we need to remove characters
  194. toPrecision = toPrecision.slice(0, index);
  195. }
  196. // Add the exponent if there is one
  197. toPrecision += exp;
  198. }
  199. else {
  200. if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
  201. // If no decimal is specified, and there are decimal places, remove trailing zeros
  202. for (var i = toPrecision.length - 1; i > 0; i--) {
  203. if (toPrecision[i] === "0") {
  204. toPrecision = toPrecision.slice(0, i);
  205. }
  206. else if (toPrecision[i] === "." || toPrecision[i] === ",") {
  207. toPrecision = toPrecision.slice(0, i);
  208. break;
  209. }
  210. else {
  211. break;
  212. }
  213. }
  214. }
  215. }
  216. return toPrecision;
  217. };
  218. /**
  219. * Check if the current value is a major value (for example when the step
  220. * is DAY, a major value is each first day of the MONTH)
  221. * @return {boolean} true if current date is major, else false.
  222. */
  223. DataStep.prototype.isMajor = function() {
  224. return (this.current % (this.scale * this.majorSteps[this.stepIndex]) === 0);
  225. };
  226. DataStep.prototype.shift = function(steps) {
  227. if (steps < 0) {
  228. for (let i = 0; i < -steps; i++) {
  229. this.previous();
  230. }
  231. }
  232. else if (steps > 0) {
  233. for (let i = 0; i < steps; i++) {
  234. this.next();
  235. }
  236. }
  237. }
  238. module.exports = DataStep;