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.

263 lines
8.4 KiB

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 -= 0.75;
  57. this._end += 1;
  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 milliseconds
  67. */
  68. DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
  69. // round to floor
  70. var size = this._end - this._start;
  71. var safeSize = size * 1.2;
  72. var minimumStepValue = minimumStep * (safeSize / containerHeight);
  73. var orderOfMagnitude = Math.round(Math.log(safeSize)/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. module.exports = DataStep;