update license comments
[dygraphs.git] / dygraph-utils.js
CommitLineData
88e95c46
DV
1/**
2 * @license
3 * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
dedb4f5f 6
004b5c90
DV
7/**
8 * @fileoverview This file contains utility functions used by dygraphs. These
9 * are typically static (i.e. not related to any particular dygraph). Examples
10 * include date/time formatting functions, basic algorithms (e.g. binary
11 * search) and generic DOM-manipulation functions.
12 */
dedb4f5f
DV
13
14Dygraph.LOG_SCALE = 10;
15Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
16
17/** @private */
18Dygraph.log10 = function(x) {
19 return Math.log(x) / Dygraph.LN_TEN;
20}
21
22// Various logging levels.
23Dygraph.DEBUG = 1;
24Dygraph.INFO = 2;
25Dygraph.WARNING = 3;
26Dygraph.ERROR = 3;
27
28// TODO(danvk): any way I can get the line numbers to be this.warn call?
29/**
30 * @private
31 * Log an error on the JS console at the given severity.
32 * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
33 * @param { String } The message to log.
34 */
35Dygraph.log = function(severity, message) {
36 if (typeof(console) != 'undefined') {
37 switch (severity) {
38 case Dygraph.DEBUG:
39 console.debug('dygraphs: ' + message);
40 break;
41 case Dygraph.INFO:
42 console.info('dygraphs: ' + message);
43 break;
44 case Dygraph.WARNING:
45 console.warn('dygraphs: ' + message);
46 break;
47 case Dygraph.ERROR:
48 console.error('dygraphs: ' + message);
49 break;
50 }
51 }
52};
53
54/** @private */
55Dygraph.info = function(message) {
56 Dygraph.log(Dygraph.INFO, message);
57};
58/** @private */
59Dygraph.prototype.info = Dygraph.info;
60
61/** @private */
62Dygraph.warn = function(message) {
63 Dygraph.log(Dygraph.WARNING, message);
64};
65/** @private */
66Dygraph.prototype.warn = Dygraph.warn;
67
68/** @private */
69Dygraph.error = function(message) {
70 Dygraph.log(Dygraph.ERROR, message);
71};
72/** @private */
73Dygraph.prototype.error = Dygraph.error;
74
75/**
76 * @private
77 * Return the 2d context for a dygraph canvas.
78 *
79 * This method is only exposed for the sake of replacing the function in
80 * automated tests, e.g.
81 *
82 * var oldFunc = Dygraph.getContext();
83 * Dygraph.getContext = function(canvas) {
84 * var realContext = oldFunc(canvas);
85 * return new Proxy(realContext);
86 * };
87 */
88Dygraph.getContext = function(canvas) {
89 return canvas.getContext("2d");
90};
91
92/**
93 * @private
94 * Add an event handler. This smooths a difference between IE and the rest of
95 * the world.
96 * @param { DOM element } el The element to add the event to.
97 * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
98 * @param { Function } fn The function to call on the event. The function takes
99 * one parameter: the event object.
100 */
101Dygraph.addEvent = function(el, evt, fn) {
102 var normed_fn = function(e) {
103 if (!e) var e = window.event;
104 fn(e);
105 };
106 if (window.addEventListener) { // Mozilla, Netscape, Firefox
107 el.addEventListener(evt, normed_fn, false);
108 } else { // IE
109 el.attachEvent('on' + evt, normed_fn);
110 }
111};
112
113/**
114 * @private
115 * Cancels further processing of an event. This is useful to prevent default
116 * browser actions, e.g. highlighting text on a double-click.
117 * Based on the article at
118 * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
119 * @param { Event } e The event whose normal behavior should be canceled.
120 */
121Dygraph.cancelEvent = function(e) {
122 e = e ? e : window.event;
123 if (e.stopPropagation) {
124 e.stopPropagation();
125 }
126 if (e.preventDefault) {
127 e.preventDefault();
128 }
129 e.cancelBubble = true;
130 e.cancel = true;
131 e.returnValue = false;
132 return false;
133};
134
135/**
136 * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
137 * is used to generate default series colors which are evenly spaced on the
138 * color wheel.
139 * @param { Number } hue Range is 0.0-1.0.
140 * @param { Number } saturation Range is 0.0-1.0.
141 * @param { Number } value Range is 0.0-1.0.
142 * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255.
143 * @private
144 */
145Dygraph.hsvToRGB = function (hue, saturation, value) {
146 var red;
147 var green;
148 var blue;
149 if (saturation === 0) {
150 red = value;
151 green = value;
152 blue = value;
153 } else {
154 var i = Math.floor(hue * 6);
155 var f = (hue * 6) - i;
156 var p = value * (1 - saturation);
157 var q = value * (1 - (saturation * f));
158 var t = value * (1 - (saturation * (1 - f)));
159 switch (i) {
160 case 1: red = q; green = value; blue = p; break;
161 case 2: red = p; green = value; blue = t; break;
162 case 3: red = p; green = q; blue = value; break;
163 case 4: red = t; green = p; blue = value; break;
164 case 5: red = value; green = p; blue = q; break;
165 case 6: // fall through
166 case 0: red = value; green = t; blue = p; break;
167 }
168 }
169 red = Math.floor(255 * red + 0.5);
170 green = Math.floor(255 * green + 0.5);
171 blue = Math.floor(255 * blue + 0.5);
172 return 'rgb(' + red + ',' + green + ',' + blue + ')';
173};
174
175// The following functions are from quirksmode.org with a modification for Safari from
176// http://blog.firetree.net/2005/07/04/javascript-find-position/
177// http://www.quirksmode.org/js/findpos.html
1bc38cbc 178// ... and modifications to support scrolling divs.
dedb4f5f 179
8442269f
RK
180/**
181 * Find the x-coordinate of the supplied object relative to the left side
182 * of the page.
183 * @private
184 */
dedb4f5f
DV
185Dygraph.findPosX = function(obj) {
186 var curleft = 0;
8442269f
RK
187 if(obj.offsetParent) {
188 var copyObj = obj;
189 while(1) {
190 curleft += copyObj.offsetLeft;
191 if(!copyObj.offsetParent) {
dedb4f5f 192 break;
8442269f
RK
193 }
194 copyObj = copyObj.offsetParent;
dedb4f5f 195 }
8442269f 196 } else if(obj.x) {
dedb4f5f 197 curleft += obj.x;
8442269f
RK
198 }
199 // This handles the case where the object is inside a scrolled div.
200 while(obj && obj != document.body) {
201 curleft -= obj.scrollLeft;
202 obj = obj.parentNode;
203 }
dedb4f5f
DV
204 return curleft;
205};
206
8442269f
RK
207/**
208 * Find the y-coordinate of the supplied object relative to the top of the
209 * page.
210 * @private
211 */
dedb4f5f
DV
212Dygraph.findPosY = function(obj) {
213 var curtop = 0;
8442269f
RK
214 if(obj.offsetParent) {
215 var copyObj = obj;
216 while(1) {
217 curtop += copyObj.offsetTop;
218 if(!copyObj.offsetParent) {
dedb4f5f 219 break;
8442269f
RK
220 }
221 copyObj = copyObj.offsetParent;
dedb4f5f 222 }
8442269f 223 } else if(obj.y) {
dedb4f5f 224 curtop += obj.y;
8442269f
RK
225 }
226 // This handles the case where the object is inside a scrolled div.
227 while(obj && obj != document.body) {
228 curtop -= obj.scrollTop;
229 obj = obj.parentNode;
230 }
dedb4f5f
DV
231 return curtop;
232};
233
234/**
235 * @private
236 * Returns the x-coordinate of the event in a coordinate system where the
237 * top-left corner of the page (not the window) is (0,0).
238 * Taken from MochiKit.Signal
239 */
240Dygraph.pageX = function(e) {
241 if (e.pageX) {
242 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
243 } else {
244 var de = document;
245 var b = document.body;
246 return e.clientX +
247 (de.scrollLeft || b.scrollLeft) -
248 (de.clientLeft || 0);
249 }
250};
251
252/**
253 * @private
254 * Returns the y-coordinate of the event in a coordinate system where the
255 * top-left corner of the page (not the window) is (0,0).
256 * Taken from MochiKit.Signal
257 */
258Dygraph.pageY = function(e) {
259 if (e.pageY) {
260 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
261 } else {
262 var de = document;
263 var b = document.body;
264 return e.clientY +
265 (de.scrollTop || b.scrollTop) -
266 (de.clientTop || 0);
267 }
268};
269
270/**
271 * @private
272 * @param { Number } x The number to consider.
273 * @return { Boolean } Whether the number is zero or NaN.
274 */
275// TODO(danvk): rename this function to something like 'isNonZeroNan'.
276Dygraph.isOK = function(x) {
277 return x && !isNaN(x);
278};
279
280/**
281 * Number formatting function which mimicks the behavior of %g in printf, i.e.
282 * either exponential or fixed format (without trailing 0s) is used depending on
283 * the length of the generated string. The advantage of this format is that
284 * there is a predictable upper bound on the resulting string length,
285 * significant figures are not dropped, and normal numbers are not displayed in
286 * exponential notation.
287 *
288 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
289 * It creates strings which are too long for absolute values between 10^-4 and
290 * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
291 * output examples.
292 *
293 * @param {Number} x The number to format
294 * @param {Number} opt_precision The precision to use, default 2.
295 * @return {String} A string formatted like %g in printf. The max generated
296 * string length should be precision + 6 (e.g 1.123e+300).
297 */
298Dygraph.floatFormat = function(x, opt_precision) {
299 // Avoid invalid precision values; [1, 21] is the valid range.
300 var p = Math.min(Math.max(1, opt_precision || 2), 21);
301
302 // This is deceptively simple. The actual algorithm comes from:
303 //
304 // Max allowed length = p + 4
305 // where 4 comes from 'e+n' and '.'.
306 //
307 // Length of fixed format = 2 + y + p
308 // where 2 comes from '0.' and y = # of leading zeroes.
309 //
310 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
311 // 1.0e-3.
312 //
313 // Since the behavior of toPrecision() is identical for larger numbers, we
314 // don't have to worry about the other bound.
315 //
316 // Finally, the argument for toExponential() is the number of trailing digits,
317 // so we take off 1 for the value before the '.'.
318 return (Math.abs(x) < 1.0e-3 && x != 0.0) ?
319 x.toExponential(p - 1) : x.toPrecision(p);
320};
321
322/**
323 * @private
324 * Converts '9' to '09' (useful for dates)
325 */
326Dygraph.zeropad = function(x) {
327 if (x < 10) return "0" + x; else return "" + x;
328};
329
330/**
331 * Return a string version of the hours, minutes and seconds portion of a date.
332 * @param {Number} date The JavaScript date (ms since epoch)
333 * @return {String} A time of the form "HH:MM:SS"
334 * @private
335 */
336Dygraph.hmsString_ = function(date) {
337 var zeropad = Dygraph.zeropad;
338 var d = new Date(date);
339 if (d.getSeconds()) {
340 return zeropad(d.getHours()) + ":" +
341 zeropad(d.getMinutes()) + ":" +
342 zeropad(d.getSeconds());
343 } else {
344 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
345 }
346};
347
348/**
dedb4f5f
DV
349 * Round a number to the specified number of digits past the decimal point.
350 * @param {Number} num The number to round
351 * @param {Number} places The number of decimals to which to round
352 * @return {Number} The rounded number
353 * @private
354 */
355Dygraph.round_ = function(num, places) {
356 var shift = Math.pow(10, places);
357 return Math.round(num * shift)/shift;
358};
359
360/**
361 * @private
362 * Implementation of binary search over an array.
363 * Currently does not work when val is outside the range of arry's values.
364 * @param { Integer } val the value to search for
365 * @param { Integer[] } arry is the value over which to search
366 * @param { Integer } abs If abs > 0, find the lowest entry greater than val
367 * If abs < 0, find the highest entry less than val.
368 * if abs == 0, find the entry that equals val.
369 * @param { Integer } [low] The first index in arry to consider (optional)
370 * @param { Integer } [high] The last index in arry to consider (optional)
371 */
372Dygraph.binarySearch = function(val, arry, abs, low, high) {
373 if (low == null || high == null) {
374 low = 0;
375 high = arry.length - 1;
376 }
377 if (low > high) {
378 return -1;
379 }
380 if (abs == null) {
381 abs = 0;
382 }
383 var validIndex = function(idx) {
384 return idx >= 0 && idx < arry.length;
385 }
386 var mid = parseInt((low + high) / 2);
387 var element = arry[mid];
388 if (element == val) {
389 return mid;
390 }
391 if (element > val) {
392 if (abs > 0) {
393 // Accept if element > val, but also if prior element < val.
394 var idx = mid - 1;
395 if (validIndex(idx) && arry[idx] < val) {
396 return mid;
397 }
398 }
399 return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
400 }
401 if (element < val) {
402 if (abs < 0) {
403 // Accept if element < val, but also if prior element > val.
404 var idx = mid + 1;
405 if (validIndex(idx) && arry[idx] > val) {
406 return mid;
407 }
408 }
409 return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
410 }
411};
412
413/**
414 * @private
415 * Parses a date, returning the number of milliseconds since epoch. This can be
416 * passed in as an xValueParser in the Dygraph constructor.
417 * TODO(danvk): enumerate formats that this understands.
418 * @param {String} A date in YYYYMMDD format.
419 * @return {Number} Milliseconds since epoch.
420 */
421Dygraph.dateParser = function(dateStr) {
422 var dateStrSlashed;
423 var d;
424 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
425 dateStrSlashed = dateStr.replace("-", "/", "g");
426 while (dateStrSlashed.search("-") != -1) {
427 dateStrSlashed = dateStrSlashed.replace("-", "/");
428 }
429 d = Dygraph.dateStrToMillis(dateStrSlashed);
430 } else if (dateStr.length == 8) { // e.g. '20090712'
431 // TODO(danvk): remove support for this format. It's confusing.
432 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
433 + "/" + dateStr.substr(6,2);
434 d = Dygraph.dateStrToMillis(dateStrSlashed);
435 } else {
436 // Any format that Date.parse will accept, e.g. "2009/07/12" or
437 // "2009/07/12 12:34:56"
438 d = Dygraph.dateStrToMillis(dateStr);
439 }
440
441 if (!d || isNaN(d)) {
442 Dygraph.error("Couldn't parse " + dateStr + " as a date");
443 }
444 return d;
445};
446
447/**
448 * @private
449 * This is identical to JavaScript's built-in Date.parse() method, except that
450 * it doesn't get replaced with an incompatible method by aggressive JS
451 * libraries like MooTools or Joomla.
452 * @param { String } str The date string, e.g. "2011/05/06"
453 * @return { Integer } millis since epoch
454 */
455Dygraph.dateStrToMillis = function(str) {
456 return new Date(str).getTime();
457};
458
459// These functions are all based on MochiKit.
460/**
461 * Copies all the properties from o to self.
462 *
463 * @private
464 */
465Dygraph.update = function (self, o) {
466 if (typeof(o) != 'undefined' && o !== null) {
467 for (var k in o) {
468 if (o.hasOwnProperty(k)) {
469 self[k] = o[k];
470 }
471 }
472 }
473 return self;
474};
475
476/**
48e614ac
DV
477 * Copies all the properties from o to self.
478 *
479 * @private
480 */
481Dygraph.updateDeep = function (self, o) {
482 if (typeof(o) != 'undefined' && o !== null) {
483 for (var k in o) {
484 if (o.hasOwnProperty(k)) {
485 if (o[k] == null) {
486 self[k] = null;
487 } else if (Dygraph.isArrayLike(o[k])) {
488 self[k] = o[k].slice();
489 } else if (typeof(o[k]) == 'object') {
490 if (typeof(self[k]) != 'object') {
491 self[k] = {};
492 }
493 Dygraph.updateDeep(self[k], o[k]);
494 } else {
495 self[k] = o[k];
496 }
497 }
498 }
499 }
500 return self;
501};
502
503/**
dedb4f5f
DV
504 * @private
505 */
506Dygraph.isArrayLike = function (o) {
507 var typ = typeof(o);
508 if (
509 (typ != 'object' && !(typ == 'function' &&
510 typeof(o.item) == 'function')) ||
511 o === null ||
512 typeof(o.length) != 'number' ||
513 o.nodeType === 3
514 ) {
515 return false;
516 }
517 return true;
518};
519
520/**
521 * @private
522 */
523Dygraph.isDateLike = function (o) {
524 if (typeof(o) != "object" || o === null ||
525 typeof(o.getTime) != 'function') {
526 return false;
527 }
528 return true;
529};
530
531/**
48e614ac 532 * Note: this only seems to work for arrays.
dedb4f5f
DV
533 * @private
534 */
535Dygraph.clone = function(o) {
536 // TODO(danvk): figure out how MochiKit's version works
537 var r = [];
538 for (var i = 0; i < o.length; i++) {
539 if (Dygraph.isArrayLike(o[i])) {
540 r.push(Dygraph.clone(o[i]));
541 } else {
542 r.push(o[i]);
543 }
544 }
545 return r;
546};
547
548/**
549 * @private
550 * Create a new canvas element. This is more complex than a simple
551 * document.createElement("canvas") because of IE and excanvas.
552 */
553Dygraph.createCanvas = function() {
554 var canvas = document.createElement("canvas");
555
556 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
557 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
558 canvas = G_vmlCanvasManager.initElement(canvas);
559 }
560
561 return canvas;
562};
9ca829f2
DV
563
564/**
565 * @private
566 * This function will scan the option list and determine if they
567 * require us to recalculate the pixel positions of each point.
568 * @param { List } a list of options to check.
569 * @return { Boolean } true if the graph needs new points else false.
570 */
571Dygraph.isPixelChangingOptionList = function(labels, attrs) {
572 // A whitelist of options that do not change pixel positions.
573 var pixelSafeOptions = {
574 'annotationClickHandler': true,
575 'annotationDblClickHandler': true,
576 'annotationMouseOutHandler': true,
577 'annotationMouseOverHandler': true,
578 'axisLabelColor': true,
579 'axisLineColor': true,
580 'axisLineWidth': true,
581 'clickCallback': true,
582 'colorSaturation': true,
583 'colorValue': true,
584 'colors': true,
585 'connectSeparatedPoints': true,
586 'digitsAfterDecimal': true,
587 'drawCallback': true,
588 'drawPoints': true,
589 'drawXGrid': true,
590 'drawYGrid': true,
591 'fillAlpha': true,
592 'gridLineColor': true,
593 'gridLineWidth': true,
594 'hideOverlayOnMouseOut': true,
595 'highlightCallback': true,
596 'highlightCircleSize': true,
597 'interactionModel': true,
598 'isZoomedIgnoreProgrammaticZoom': true,
599 'labelsDiv': true,
600 'labelsDivStyles': true,
601 'labelsDivWidth': true,
602 'labelsKMB': true,
603 'labelsKMG2': true,
604 'labelsSeparateLines': true,
605 'labelsShowZeroValues': true,
606 'legend': true,
607 'maxNumberWidth': true,
608 'panEdgeFraction': true,
609 'pixelsPerYLabel': true,
610 'pointClickCallback': true,
611 'pointSize': true,
612 'showLabelsOnHighlight': true,
613 'showRoller': true,
614 'sigFigs': true,
615 'strokeWidth': true,
616 'underlayCallback': true,
617 'unhighlightCallback': true,
618 'xAxisLabelFormatter': true,
619 'xTicker': true,
620 'xValueFormatter': true,
621 'yAxisLabelFormatter': true,
622 'yValueFormatter': true,
623 'zoomCallback': true
624 };
625
626 // Assume that we do not require new points.
627 // This will change to true if we actually do need new points.
628 var requiresNewPoints = false;
629
630 // Create a dictionary of series names for faster lookup.
631 // If there are no labels, then the dictionary stays empty.
632 var seriesNamesDictionary = { };
633 if (labels) {
634 for (var i = 1; i < labels.length; i++) {
635 seriesNamesDictionary[labels[i]] = true;
636 }
637 }
638
639 // Iterate through the list of updated options.
640 for (property in attrs) {
641 // Break early if we already know we need new points from a previous option.
642 if (requiresNewPoints) {
643 break;
644 }
645 if (attrs.hasOwnProperty(property)) {
646 // Find out of this field is actually a series specific options list.
647 if (seriesNamesDictionary[property]) {
648 // This property value is a list of options for this series.
649 // If any of these sub properties are not pixel safe, set the flag.
650 for (subProperty in attrs[property]) {
651 // Break early if we already know we need new points from a previous option.
652 if (requiresNewPoints) {
653 break;
654 }
655 if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
656 requiresNewPoints = true;
657 }
658 }
659 // If this was not a series specific option list, check if its a pixel changing property.
660 } else if (!pixelSafeOptions[property]) {
661 requiresNewPoints = true;
662 }
663 }
664 }
665
666 return requiresNewPoints;
667};