Fix Issue 162: Support DOS-style line endings
[dygraphs.git] / dygraph-utils.js
1 /**
2 * @license
3 * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
6
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 */
13
14 /*jshint globalstrict: true */
15 /*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */
16 "use strict";
17
18 Dygraph.LOG_SCALE = 10;
19 Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
20
21 /** @private */
22 Dygraph.log10 = function(x) {
23 return Math.log(x) / Dygraph.LN_TEN;
24 };
25
26 // Various logging levels.
27 Dygraph.DEBUG = 1;
28 Dygraph.INFO = 2;
29 Dygraph.WARNING = 3;
30 Dygraph.ERROR = 3;
31
32 // Set this to log stack traces on warnings, etc.
33 // This requires stacktrace.js, which is up to you to provide.
34 // A copy can be found in the dygraphs repo, or at
35 // https://github.com/eriwen/javascript-stacktrace
36 Dygraph.LOG_STACK_TRACES = false;
37
38 /** A dotted line stroke pattern. */
39 Dygraph.DOTTED_LINE = [2, 2];
40 /** A dashed line stroke pattern. */
41 Dygraph.DASHED_LINE = [7, 3];
42 /** A dot dash stroke pattern. */
43 Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
44
45 /**
46 * @private
47 * Log an error on the JS console at the given severity.
48 * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
49 * @param { String } The message to log.
50 */
51 Dygraph.log = function(severity, message) {
52 var st;
53 if (typeof(printStackTrace) != 'undefined') {
54 try {
55 // Remove uninteresting bits: logging functions and paths.
56 st = printStackTrace({guess:false});
57 while (st[0].indexOf("stacktrace") != -1) {
58 st.splice(0, 1);
59 }
60
61 st.splice(0, 2);
62 for (var i = 0; i < st.length; i++) {
63 st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1')
64 .replace(/\@.*\/([^\/]*)/, '@$1')
65 .replace('[object Object].', '');
66 }
67 var top_msg = st.splice(0, 1)[0];
68 message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')';
69 } catch(e) {
70 // Oh well, it was worth a shot!
71 }
72 }
73
74 if (typeof(console) != 'undefined') {
75 switch (severity) {
76 case Dygraph.DEBUG:
77 console.debug('dygraphs: ' + message);
78 break;
79 case Dygraph.INFO:
80 console.info('dygraphs: ' + message);
81 break;
82 case Dygraph.WARNING:
83 console.warn('dygraphs: ' + message);
84 break;
85 case Dygraph.ERROR:
86 console.error('dygraphs: ' + message);
87 break;
88 }
89 }
90
91 if (Dygraph.LOG_STACK_TRACES) {
92 console.log(st.join('\n'));
93 }
94 };
95
96 /** @private */
97 Dygraph.info = function(message) {
98 Dygraph.log(Dygraph.INFO, message);
99 };
100 /** @private */
101 Dygraph.prototype.info = Dygraph.info;
102
103 /** @private */
104 Dygraph.warn = function(message) {
105 Dygraph.log(Dygraph.WARNING, message);
106 };
107 /** @private */
108 Dygraph.prototype.warn = Dygraph.warn;
109
110 /** @private */
111 Dygraph.error = function(message) {
112 Dygraph.log(Dygraph.ERROR, message);
113 };
114 /** @private */
115 Dygraph.prototype.error = Dygraph.error;
116
117 /**
118 * @private
119 * Return the 2d context for a dygraph canvas.
120 *
121 * This method is only exposed for the sake of replacing the function in
122 * automated tests, e.g.
123 *
124 * var oldFunc = Dygraph.getContext();
125 * Dygraph.getContext = function(canvas) {
126 * var realContext = oldFunc(canvas);
127 * return new Proxy(realContext);
128 * };
129 */
130 Dygraph.getContext = function(canvas) {
131 return canvas.getContext("2d");
132 };
133
134 /**
135 * @private
136 * Add an event handler. This smooths a difference between IE and the rest of
137 * the world.
138 * @param { DOM element } elem The element to add the event to.
139 * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
140 * @param { Function } fn The function to call on the event. The function takes
141 * one parameter: the event object.
142 */
143 Dygraph.addEvent = function addEvent(elem, type, fn) {
144 if (elem.addEventListener) {
145 elem.addEventListener(type, fn, false);
146 } else {
147 elem[type+fn] = function(){fn(window.event);};
148 elem.attachEvent('on'+type, elem[type+fn]);
149 }
150 };
151
152 /**
153 * @private
154 * Add an event handler. This event handler is kept until the graph is
155 * destroyed with a call to graph.destroy().
156 *
157 * @param { DOM element } elem The element to add the event to.
158 * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
159 * @param { Function } fn The function to call on the event. The function takes
160 * one parameter: the event object.
161 */
162 Dygraph.prototype.addEvent = function addEvent(elem, type, fn) {
163 Dygraph.addEvent(elem, type, fn);
164 this.registeredEvents_.push({ elem : elem, type : type, fn : fn });
165 };
166
167 /**
168 * @private
169 * Remove an event handler. This smooths a difference between IE and the rest of
170 * the world.
171 * @param { DOM element } elem The element to add the event to.
172 * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
173 * @param { Function } fn The function to call on the event. The function takes
174 * one parameter: the event object.
175 */
176 Dygraph.removeEvent = function addEvent(elem, type, fn) {
177 if (elem.removeEventListener) {
178 elem.removeEventListener(type, fn, false);
179 } else {
180 try {
181 elem.detachEvent('on'+type, elem[type+fn]);
182 } catch(e) {
183 // We only detach event listeners on a "best effort" basis in IE. See:
184 // http://stackoverflow.com/questions/2553632/detachevent-not-working-with-named-inline-functions
185 }
186 elem[type+fn] = null;
187 }
188 };
189
190 /**
191 * @private
192 * Cancels further processing of an event. This is useful to prevent default
193 * browser actions, e.g. highlighting text on a double-click.
194 * Based on the article at
195 * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
196 * @param { Event } e The event whose normal behavior should be canceled.
197 */
198 Dygraph.cancelEvent = function(e) {
199 e = e ? e : window.event;
200 if (e.stopPropagation) {
201 e.stopPropagation();
202 }
203 if (e.preventDefault) {
204 e.preventDefault();
205 }
206 e.cancelBubble = true;
207 e.cancel = true;
208 e.returnValue = false;
209 return false;
210 };
211
212 /**
213 * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
214 * is used to generate default series colors which are evenly spaced on the
215 * color wheel.
216 * @param { Number } hue Range is 0.0-1.0.
217 * @param { Number } saturation Range is 0.0-1.0.
218 * @param { Number } value Range is 0.0-1.0.
219 * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255.
220 * @private
221 */
222 Dygraph.hsvToRGB = function (hue, saturation, value) {
223 var red;
224 var green;
225 var blue;
226 if (saturation === 0) {
227 red = value;
228 green = value;
229 blue = value;
230 } else {
231 var i = Math.floor(hue * 6);
232 var f = (hue * 6) - i;
233 var p = value * (1 - saturation);
234 var q = value * (1 - (saturation * f));
235 var t = value * (1 - (saturation * (1 - f)));
236 switch (i) {
237 case 1: red = q; green = value; blue = p; break;
238 case 2: red = p; green = value; blue = t; break;
239 case 3: red = p; green = q; blue = value; break;
240 case 4: red = t; green = p; blue = value; break;
241 case 5: red = value; green = p; blue = q; break;
242 case 6: // fall through
243 case 0: red = value; green = t; blue = p; break;
244 }
245 }
246 red = Math.floor(255 * red + 0.5);
247 green = Math.floor(255 * green + 0.5);
248 blue = Math.floor(255 * blue + 0.5);
249 return 'rgb(' + red + ',' + green + ',' + blue + ')';
250 };
251
252 // The following functions are from quirksmode.org with a modification for Safari from
253 // http://blog.firetree.net/2005/07/04/javascript-find-position/
254 // http://www.quirksmode.org/js/findpos.html
255 // ... and modifications to support scrolling divs.
256
257 /**
258 * Find the x-coordinate of the supplied object relative to the left side
259 * of the page.
260 * @private
261 */
262 Dygraph.findPosX = function(obj) {
263 var curleft = 0;
264 if(obj.offsetParent) {
265 var copyObj = obj;
266 while(1) {
267 curleft += copyObj.offsetLeft;
268 if(!copyObj.offsetParent) {
269 break;
270 }
271 copyObj = copyObj.offsetParent;
272 }
273 } else if(obj.x) {
274 curleft += obj.x;
275 }
276 // This handles the case where the object is inside a scrolled div.
277 while(obj && obj != document.body) {
278 curleft -= obj.scrollLeft;
279 obj = obj.parentNode;
280 }
281 return curleft;
282 };
283
284 /**
285 * Find the y-coordinate of the supplied object relative to the top of the
286 * page.
287 * @private
288 */
289 Dygraph.findPosY = function(obj) {
290 var curtop = 0;
291 if(obj.offsetParent) {
292 var copyObj = obj;
293 while(1) {
294 curtop += copyObj.offsetTop;
295 if(!copyObj.offsetParent) {
296 break;
297 }
298 copyObj = copyObj.offsetParent;
299 }
300 } else if(obj.y) {
301 curtop += obj.y;
302 }
303 // This handles the case where the object is inside a scrolled div.
304 while(obj && obj != document.body) {
305 curtop -= obj.scrollTop;
306 obj = obj.parentNode;
307 }
308 return curtop;
309 };
310
311 /**
312 * @private
313 * Returns the x-coordinate of the event in a coordinate system where the
314 * top-left corner of the page (not the window) is (0,0).
315 * Taken from MochiKit.Signal
316 */
317 Dygraph.pageX = function(e) {
318 if (e.pageX) {
319 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
320 } else {
321 var de = document;
322 var b = document.body;
323 return e.clientX +
324 (de.scrollLeft || b.scrollLeft) -
325 (de.clientLeft || 0);
326 }
327 };
328
329 /**
330 * @private
331 * Returns the y-coordinate of the event in a coordinate system where the
332 * top-left corner of the page (not the window) is (0,0).
333 * Taken from MochiKit.Signal
334 */
335 Dygraph.pageY = function(e) {
336 if (e.pageY) {
337 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
338 } else {
339 var de = document;
340 var b = document.body;
341 return e.clientY +
342 (de.scrollTop || b.scrollTop) -
343 (de.clientTop || 0);
344 }
345 };
346
347 /**
348 * @private
349 * @param { Number } x The number to consider.
350 * @return { Boolean } Whether the number is zero or NaN.
351 */
352 // TODO(danvk): rename this function to something like 'isNonZeroNan'.
353 // TODO(danvk): determine when else this returns false (e.g. for undefined or null)
354 Dygraph.isOK = function(x) {
355 return x && !isNaN(x);
356 };
357
358 /**
359 * @private
360 * @param { Object } p The point to consider, valid points are {x, y} objects
361 * @param { Boolean } allowNaNY Treat point with y=NaN as valid
362 * @return { Boolean } Whether the point has numeric x and y.
363 */
364 Dygraph.isValidPoint = function(p, allowNaNY) {
365 if (!p) return false; // null or undefined object
366 if (p.yval === null) return false; // missing point
367 if (p.x === null || p.x === undefined) return false;
368 if (p.y === null || p.y === undefined) return false;
369 if (isNaN(p.x) || (!allowNaNY && isNaN(p.y))) return false;
370 return true;
371 };
372
373 /**
374 * Number formatting function which mimicks the behavior of %g in printf, i.e.
375 * either exponential or fixed format (without trailing 0s) is used depending on
376 * the length of the generated string. The advantage of this format is that
377 * there is a predictable upper bound on the resulting string length,
378 * significant figures are not dropped, and normal numbers are not displayed in
379 * exponential notation.
380 *
381 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
382 * It creates strings which are too long for absolute values between 10^-4 and
383 * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
384 * output examples.
385 *
386 * @param {Number} x The number to format
387 * @param {Number} opt_precision The precision to use, default 2.
388 * @return {String} A string formatted like %g in printf. The max generated
389 * string length should be precision + 6 (e.g 1.123e+300).
390 */
391 Dygraph.floatFormat = function(x, opt_precision) {
392 // Avoid invalid precision values; [1, 21] is the valid range.
393 var p = Math.min(Math.max(1, opt_precision || 2), 21);
394
395 // This is deceptively simple. The actual algorithm comes from:
396 //
397 // Max allowed length = p + 4
398 // where 4 comes from 'e+n' and '.'.
399 //
400 // Length of fixed format = 2 + y + p
401 // where 2 comes from '0.' and y = # of leading zeroes.
402 //
403 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
404 // 1.0e-3.
405 //
406 // Since the behavior of toPrecision() is identical for larger numbers, we
407 // don't have to worry about the other bound.
408 //
409 // Finally, the argument for toExponential() is the number of trailing digits,
410 // so we take off 1 for the value before the '.'.
411 return (Math.abs(x) < 1.0e-3 && x !== 0.0) ?
412 x.toExponential(p - 1) : x.toPrecision(p);
413 };
414
415 /**
416 * @private
417 * Converts '9' to '09' (useful for dates)
418 */
419 Dygraph.zeropad = function(x) {
420 if (x < 10) return "0" + x; else return "" + x;
421 };
422
423 /**
424 * Return a string version of the hours, minutes and seconds portion of a date.
425 * @param {Number} date The JavaScript date (ms since epoch)
426 * @return {String} A time of the form "HH:MM:SS"
427 * @private
428 */
429 Dygraph.hmsString_ = function(date) {
430 var zeropad = Dygraph.zeropad;
431 var d = new Date(date);
432 if (d.getSeconds()) {
433 return zeropad(d.getHours()) + ":" +
434 zeropad(d.getMinutes()) + ":" +
435 zeropad(d.getSeconds());
436 } else {
437 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
438 }
439 };
440
441 /**
442 * Round a number to the specified number of digits past the decimal point.
443 * @param {Number} num The number to round
444 * @param {Number} places The number of decimals to which to round
445 * @return {Number} The rounded number
446 * @private
447 */
448 Dygraph.round_ = function(num, places) {
449 var shift = Math.pow(10, places);
450 return Math.round(num * shift)/shift;
451 };
452
453 /**
454 * @private
455 * Implementation of binary search over an array.
456 * Currently does not work when val is outside the range of arry's values.
457 * @param { Integer } val the value to search for
458 * @param { Integer[] } arry is the value over which to search
459 * @param { Integer } abs If abs > 0, find the lowest entry greater than val
460 * If abs < 0, find the highest entry less than val.
461 * if abs == 0, find the entry that equals val.
462 * @param { Integer } [low] The first index in arry to consider (optional)
463 * @param { Integer } [high] The last index in arry to consider (optional)
464 */
465 Dygraph.binarySearch = function(val, arry, abs, low, high) {
466 if (low === null || low === undefined ||
467 high === null || high === undefined) {
468 low = 0;
469 high = arry.length - 1;
470 }
471 if (low > high) {
472 return -1;
473 }
474 if (abs === null || abs === undefined) {
475 abs = 0;
476 }
477 var validIndex = function(idx) {
478 return idx >= 0 && idx < arry.length;
479 };
480 var mid = parseInt((low + high) / 2, 10);
481 var element = arry[mid];
482 if (element == val) {
483 return mid;
484 }
485
486 var idx;
487 if (element > val) {
488 if (abs > 0) {
489 // Accept if element > val, but also if prior element < val.
490 idx = mid - 1;
491 if (validIndex(idx) && arry[idx] < val) {
492 return mid;
493 }
494 }
495 return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
496 }
497 if (element < val) {
498 if (abs < 0) {
499 // Accept if element < val, but also if prior element > val.
500 idx = mid + 1;
501 if (validIndex(idx) && arry[idx] > val) {
502 return mid;
503 }
504 }
505 return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
506 }
507 };
508
509 /**
510 * @private
511 * Parses a date, returning the number of milliseconds since epoch. This can be
512 * passed in as an xValueParser in the Dygraph constructor.
513 * TODO(danvk): enumerate formats that this understands.
514 * @param {String} A date in YYYYMMDD format.
515 * @return {Number} Milliseconds since epoch.
516 */
517 Dygraph.dateParser = function(dateStr) {
518 var dateStrSlashed;
519 var d;
520
521 // Let the system try the format first, with one caveat:
522 // YYYY-MM-DD[ HH:MM:SS] is interpreted as UTC by a variety of browsers.
523 // dygraphs displays dates in local time, so this will result in surprising
524 // inconsistencies. But if you specify "T" or "Z" (i.e. YYYY-MM-DDTHH:MM:SS),
525 // then you probably know what you're doing, so we'll let you go ahead.
526 // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255
527 if (dateStr.search("-") == -1 ||
528 dateStr.search("T") != -1 || dateStr.search("Z") != -1) {
529 d = Dygraph.dateStrToMillis(dateStr);
530 if (d && !isNaN(d)) return d;
531 }
532
533 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
534 dateStrSlashed = dateStr.replace("-", "/", "g");
535 while (dateStrSlashed.search("-") != -1) {
536 dateStrSlashed = dateStrSlashed.replace("-", "/");
537 }
538 d = Dygraph.dateStrToMillis(dateStrSlashed);
539 } else if (dateStr.length == 8) { // e.g. '20090712'
540 // TODO(danvk): remove support for this format. It's confusing.
541 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" +
542 dateStr.substr(6,2);
543 d = Dygraph.dateStrToMillis(dateStrSlashed);
544 } else {
545 // Any format that Date.parse will accept, e.g. "2009/07/12" or
546 // "2009/07/12 12:34:56"
547 d = Dygraph.dateStrToMillis(dateStr);
548 }
549
550 if (!d || isNaN(d)) {
551 Dygraph.error("Couldn't parse " + dateStr + " as a date");
552 }
553 return d;
554 };
555
556 /**
557 * @private
558 * This is identical to JavaScript's built-in Date.parse() method, except that
559 * it doesn't get replaced with an incompatible method by aggressive JS
560 * libraries like MooTools or Joomla.
561 * @param { String } str The date string, e.g. "2011/05/06"
562 * @return { Integer } millis since epoch
563 */
564 Dygraph.dateStrToMillis = function(str) {
565 return new Date(str).getTime();
566 };
567
568 // These functions are all based on MochiKit.
569 /**
570 * Copies all the properties from o to self.
571 *
572 * @private
573 */
574 Dygraph.update = function (self, o) {
575 if (typeof(o) != 'undefined' && o !== null) {
576 for (var k in o) {
577 if (o.hasOwnProperty(k)) {
578 self[k] = o[k];
579 }
580 }
581 }
582 return self;
583 };
584
585 /**
586 * Copies all the properties from o to self.
587 *
588 * @private
589 */
590 Dygraph.updateDeep = function (self, o) {
591 // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
592 function isNode(o) {
593 return (
594 typeof Node === "object" ? o instanceof Node :
595 typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
596 );
597 }
598
599 if (typeof(o) != 'undefined' && o !== null) {
600 for (var k in o) {
601 if (o.hasOwnProperty(k)) {
602 if (o[k] === null) {
603 self[k] = null;
604 } else if (Dygraph.isArrayLike(o[k])) {
605 self[k] = o[k].slice();
606 } else if (isNode(o[k])) {
607 // DOM objects are shallowly-copied.
608 self[k] = o[k];
609 } else if (typeof(o[k]) == 'object') {
610 if (typeof(self[k]) != 'object' || self[k] === null) {
611 self[k] = {};
612 }
613 Dygraph.updateDeep(self[k], o[k]);
614 } else {
615 self[k] = o[k];
616 }
617 }
618 }
619 }
620 return self;
621 };
622
623 /**
624 * @private
625 */
626 Dygraph.isArrayLike = function (o) {
627 var typ = typeof(o);
628 if (
629 (typ != 'object' && !(typ == 'function' &&
630 typeof(o.item) == 'function')) ||
631 o === null ||
632 typeof(o.length) != 'number' ||
633 o.nodeType === 3
634 ) {
635 return false;
636 }
637 return true;
638 };
639
640 /**
641 * @private
642 */
643 Dygraph.isDateLike = function (o) {
644 if (typeof(o) != "object" || o === null ||
645 typeof(o.getTime) != 'function') {
646 return false;
647 }
648 return true;
649 };
650
651 /**
652 * Note: this only seems to work for arrays.
653 * @private
654 */
655 Dygraph.clone = function(o) {
656 // TODO(danvk): figure out how MochiKit's version works
657 var r = [];
658 for (var i = 0; i < o.length; i++) {
659 if (Dygraph.isArrayLike(o[i])) {
660 r.push(Dygraph.clone(o[i]));
661 } else {
662 r.push(o[i]);
663 }
664 }
665 return r;
666 };
667
668 /**
669 * @private
670 * Create a new canvas element. This is more complex than a simple
671 * document.createElement("canvas") because of IE and excanvas.
672 */
673 Dygraph.createCanvas = function() {
674 var canvas = document.createElement("canvas");
675
676 var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
677 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
678 canvas = G_vmlCanvasManager.initElement(canvas);
679 }
680
681 return canvas;
682 };
683
684 /**
685 * @private
686 * Checks whether the user is on an Android browser.
687 * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
688 */
689 Dygraph.isAndroid = function() {
690 return (/Android/).test(navigator.userAgent);
691 };
692
693 Dygraph.Iterator = function(array, start, length, predicate) {
694 start = start || 0;
695 length = length || array.length;
696 this.hasNext = true; // Use to identify if there's another element.
697 this.peek = null; // Use for look-ahead
698 this.start_ = start;
699 this.array_ = array;
700 this.predicate_ = predicate;
701 this.end_ = Math.min(array.length, start + length);
702 this.nextIdx_ = start - 1; // use -1 so initial advance works.
703 this.next(); // ignoring result.
704 };
705
706 Dygraph.Iterator.prototype.next = function() {
707 if (!this.hasNext) {
708 return null;
709 }
710 var obj = this.peek;
711
712 var nextIdx = this.nextIdx_ + 1;
713 var found = false;
714 while (nextIdx < this.end_) {
715 if (!this.predicate_ || this.predicate_(this.array_, nextIdx)) {
716 this.peek = this.array_[nextIdx];
717 found = true;
718 break;
719 }
720 nextIdx++;
721 }
722 this.nextIdx_ = nextIdx;
723 if (!found) {
724 this.hasNext = false;
725 this.peek = null;
726 }
727 return obj;
728 };
729
730 /**
731 * @private
732 * Returns a new iterator over array, between indexes start and
733 * start + length, and only returns entries that pass the accept function
734 *
735 * @param array the array to iterate over.
736 * @param start the first index to iterate over, 0 if absent.
737 * @param length the number of elements in the array to iterate over.
738 * This, along with start, defines a slice of the array, and so length
739 * doesn't imply the number of elements in the iterator when accept
740 * doesn't always accept all values. array.length when absent.
741 * @param predicate a function that takes parameters array and idx, which
742 * returns true when the element should be returned. If omitted, all
743 * elements are accepted.
744 */
745 Dygraph.createIterator = function(array, start, length, predicate) {
746 return new Dygraph.Iterator(array, start, length, predicate);
747 };
748
749 /**
750 * @private
751 * Call a function N times at a given interval, then call a cleanup function
752 * once. repeat_fn is called once immediately, then (times - 1) times
753 * asynchronously. If times=1, then cleanup_fn() is also called synchronously.
754 * @param repeat_fn {Function} Called repeatedly -- takes the number of calls
755 * (from 0 to times-1) as an argument.
756 * @param times {number} The number of times to call repeat_fn
757 * @param every_ms {number} Milliseconds between calls
758 * @param cleanup_fn {Function} A function to call after all repeat_fn calls.
759 * @private
760 */
761 Dygraph.repeatAndCleanup = function(repeat_fn, times, every_ms, cleanup_fn) {
762 var count = 0;
763 var start_time = new Date().getTime();
764 repeat_fn(count);
765 if (times == 1) {
766 cleanup_fn();
767 return;
768 }
769
770 (function loop() {
771 if (count >= times) return;
772 var target_time = start_time + (1 + count) * every_ms;
773 setTimeout(function() {
774 count++;
775 repeat_fn(count);
776 if (count >= times - 1) {
777 cleanup_fn();
778 } else {
779 loop();
780 }
781 }, target_time - new Date().getTime());
782 // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
783 })();
784 };
785
786 /**
787 * @private
788 * This function will scan the option list and determine if they
789 * require us to recalculate the pixel positions of each point.
790 * @param { List } a list of options to check.
791 * @return { Boolean } true if the graph needs new points else false.
792 */
793 Dygraph.isPixelChangingOptionList = function(labels, attrs) {
794 // A whitelist of options that do not change pixel positions.
795 var pixelSafeOptions = {
796 'annotationClickHandler': true,
797 'annotationDblClickHandler': true,
798 'annotationMouseOutHandler': true,
799 'annotationMouseOverHandler': true,
800 'axisLabelColor': true,
801 'axisLineColor': true,
802 'axisLineWidth': true,
803 'clickCallback': true,
804 'digitsAfterDecimal': true,
805 'drawCallback': true,
806 'drawHighlightPointCallback': true,
807 'drawPoints': true,
808 'drawPointCallback': true,
809 'drawXGrid': true,
810 'drawYGrid': true,
811 'fillAlpha': true,
812 'gridLineColor': true,
813 'gridLineWidth': true,
814 'hideOverlayOnMouseOut': true,
815 'highlightCallback': true,
816 'highlightCircleSize': true,
817 'interactionModel': true,
818 'isZoomedIgnoreProgrammaticZoom': true,
819 'labelsDiv': true,
820 'labelsDivStyles': true,
821 'labelsDivWidth': true,
822 'labelsKMB': true,
823 'labelsKMG2': true,
824 'labelsSeparateLines': true,
825 'labelsShowZeroValues': true,
826 'legend': true,
827 'maxNumberWidth': true,
828 'panEdgeFraction': true,
829 'pixelsPerYLabel': true,
830 'pointClickCallback': true,
831 'pointSize': true,
832 'rangeSelectorPlotFillColor': true,
833 'rangeSelectorPlotStrokeColor': true,
834 'showLabelsOnHighlight': true,
835 'showRoller': true,
836 'sigFigs': true,
837 'strokeWidth': true,
838 'underlayCallback': true,
839 'unhighlightCallback': true,
840 'xAxisLabelFormatter': true,
841 'xTicker': true,
842 'xValueFormatter': true,
843 'yAxisLabelFormatter': true,
844 'yValueFormatter': true,
845 'zoomCallback': true
846 };
847
848 // Assume that we do not require new points.
849 // This will change to true if we actually do need new points.
850 var requiresNewPoints = false;
851
852 // Create a dictionary of series names for faster lookup.
853 // If there are no labels, then the dictionary stays empty.
854 var seriesNamesDictionary = { };
855 if (labels) {
856 for (var i = 1; i < labels.length; i++) {
857 seriesNamesDictionary[labels[i]] = true;
858 }
859 }
860
861 // Iterate through the list of updated options.
862 for (var property in attrs) {
863 // Break early if we already know we need new points from a previous option.
864 if (requiresNewPoints) {
865 break;
866 }
867 if (attrs.hasOwnProperty(property)) {
868 // Find out of this field is actually a series specific options list.
869 if (seriesNamesDictionary[property]) {
870 // This property value is a list of options for this series.
871 // If any of these sub properties are not pixel safe, set the flag.
872 for (var subProperty in attrs[property]) {
873 // Break early if we already know we need new points from a previous option.
874 if (requiresNewPoints) {
875 break;
876 }
877 if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
878 requiresNewPoints = true;
879 }
880 }
881 // If this was not a series specific option list, check if its a pixel changing property.
882 } else if (!pixelSafeOptions[property]) {
883 requiresNewPoints = true;
884 }
885 }
886 }
887
888 return requiresNewPoints;
889 };
890
891 /**
892 * Compares two arrays to see if they are equal. If either parameter is not an
893 * array it will return false. Does a shallow compare
894 * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
895 * @param array1 first array
896 * @param array2 second array
897 * @return True if both parameters are arrays, and contents are equal.
898 */
899 Dygraph.compareArrays = function(array1, array2) {
900 if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
901 return false;
902 }
903 if (array1.length !== array2.length) {
904 return false;
905 }
906 for (var i = 0; i < array1.length; i++) {
907 if (array1[i] !== array2[i]) {
908 return false;
909 }
910 }
911 return true;
912 };
913
914 /**
915 * ctx: the canvas context
916 * sides: the number of sides in the shape.
917 * radius: the radius of the image.
918 * cx: center x coordate
919 * cy: center y coordinate
920 * rotationRadians: the shift of the initial angle, in radians.
921 * delta: the angle shift for each line. If missing, creates a regular
922 * polygon.
923 */
924 Dygraph.regularShape_ = function(
925 ctx, sides, radius, cx, cy, rotationRadians, delta) {
926 rotationRadians = rotationRadians ? rotationRadians : 0;
927 delta = delta ? delta : Math.PI * 2 / sides;
928
929 ctx.beginPath();
930 var first = true;
931 var initialAngle = rotationRadians;
932 var angle = initialAngle;
933
934 var computeCoordinates = function() {
935 var x = cx + (Math.sin(angle) * radius);
936 var y = cy + (-Math.cos(angle) * radius);
937 return [x, y];
938 };
939
940 var initialCoordinates = computeCoordinates();
941 var x = initialCoordinates[0];
942 var y = initialCoordinates[1];
943 ctx.moveTo(x, y);
944
945 for (var idx = 0; idx < sides; idx++) {
946 angle = (idx == sides - 1) ? initialAngle : (angle + delta);
947 var coords = computeCoordinates();
948 ctx.lineTo(coords[0], coords[1]);
949 }
950 ctx.fill();
951 ctx.stroke();
952 };
953
954 Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) {
955 return function(g, name, ctx, cx, cy, color, radius) {
956 ctx.strokeStyle = color;
957 ctx.fillStyle = "white";
958 Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta);
959 };
960 };
961
962 Dygraph.DrawPolygon_ = function(sides, rotationRadians, ctx, cx, cy, color, radius, delta) {
963 new Dygraph.RegularShape_(sides, rotationRadians, delta).draw(ctx, cx, cy, radius);
964 };
965
966 Dygraph.Circles = {
967 DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
968 ctx.beginPath();
969 ctx.fillStyle = color;
970 ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
971 ctx.fill();
972 },
973 TRIANGLE : Dygraph.shapeFunction_(3),
974 SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4),
975 DIAMOND : Dygraph.shapeFunction_(4),
976 PENTAGON : Dygraph.shapeFunction_(5),
977 HEXAGON : Dygraph.shapeFunction_(6),
978 CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
979 ctx.beginPath();
980 ctx.strokeStyle = color;
981 ctx.fillStyle = "white";
982 ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
983 ctx.fill();
984 ctx.stroke();
985 },
986 STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5),
987 PLUS : function(g, name, ctx, cx, cy, color, radius) {
988 ctx.strokeStyle = color;
989
990 ctx.beginPath();
991 ctx.moveTo(cx + radius, cy);
992 ctx.lineTo(cx - radius, cy);
993 ctx.closePath();
994 ctx.stroke();
995
996 ctx.beginPath();
997 ctx.moveTo(cx, cy + radius);
998 ctx.lineTo(cx, cy - radius);
999 ctx.closePath();
1000 ctx.stroke();
1001 },
1002 EX : function(g, name, ctx, cx, cy, color, radius) {
1003 ctx.strokeStyle = color;
1004
1005 ctx.beginPath();
1006 ctx.moveTo(cx + radius, cy + radius);
1007 ctx.lineTo(cx - radius, cy - radius);
1008 ctx.closePath();
1009 ctx.stroke();
1010
1011 ctx.beginPath();
1012 ctx.moveTo(cx + radius, cy - radius);
1013 ctx.lineTo(cx - radius, cy + radius);
1014 ctx.closePath();
1015 ctx.stroke();
1016 }
1017 };
1018
1019 /**
1020 * To create a "drag" interaction, you typically register a mousedown event
1021 * handler on the element where the drag begins. In that handler, you register a
1022 * mouseup handler on the window to determine when the mouse is released,
1023 * wherever that release happens. This works well, except when the user releases
1024 * the mouse over an off-domain iframe. In that case, the mouseup event is
1025 * handled by the iframe and never bubbles up to the window handler.
1026 *
1027 * To deal with this issue, we cover iframes with high z-index divs to make sure
1028 * they don't capture mouseup.
1029 *
1030 * Usage:
1031 * element.addEventListener('mousedown', function() {
1032 * var tarper = new Dygraph.IFrameTarp();
1033 * tarper.cover();
1034 * var mouseUpHandler = function() {
1035 * ...
1036 * window.removeEventListener(mouseUpHandler);
1037 * tarper.uncover();
1038 * };
1039 * window.addEventListener('mouseup', mouseUpHandler);
1040 * };
1041 *
1042 *
1043 * @constructor
1044 */
1045 Dygraph.IFrameTarp = function() {
1046 this.tarps = [];
1047 };
1048
1049 /**
1050 * Find all the iframes in the document and cover them with high z-index
1051 * transparent divs.
1052 */
1053 Dygraph.IFrameTarp.prototype.cover = function() {
1054 var iframes = document.getElementsByTagName("iframe");
1055 for (var i = 0; i < iframes.length; i++) {
1056 var iframe = iframes[i];
1057 var x = Dygraph.findPosX(iframe),
1058 y = Dygraph.findPosY(iframe),
1059 width = iframe.offsetWidth,
1060 height = iframe.offsetHeight;
1061
1062 var div = document.createElement("div");
1063 div.style.position = "absolute";
1064 div.style.left = x + 'px';
1065 div.style.top = y + 'px';
1066 div.style.width = width + 'px';
1067 div.style.height = height + 'px';
1068 div.style.zIndex = 999;
1069 document.body.appendChild(div);
1070 this.tarps.push(div);
1071 }
1072 };
1073
1074 /**
1075 * Remove all the iframe covers. You should call this in a mouseup handler.
1076 */
1077 Dygraph.IFrameTarp.prototype.uncover = function() {
1078 for (var i = 0; i < this.tarps.length; i++) {
1079 this.tarps[i].parentNode.removeChild(this.tarps[i]);
1080 }
1081 this.tarps = [];
1082 };
1083
1084 /**
1085 * Determine whether |data| is delimited by CR, LF or CRLF.
1086 * @param {string} data
1087 * @return {string|null} the delimiter that was detected.
1088 */
1089 Dygraph.detectLineDelimiter = function(data) {
1090 for (var i = 0; i < data.length; i++) {
1091 var code = data[i];
1092 if (code == '\r') return code;
1093 if (code == '\n') {
1094 // Might actually be "\n\r".
1095 if (i < data.length && data[i + 1] == '\r') return '\n\r';
1096 return code;
1097 }
1098 }
1099
1100 return null;
1101 };