X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-utils.js;h=1d1623fe5d5914075b8e34e6b924d08345d03c69;hb=ccecde931c59699a62c0e83651dc28ac680f83ab;hp=bf488efd02eb59467a13ee0399fd0473bc0d5220;hpb=5cb6204347bf9e4ad2a1d243a40caea2bf8c54e7;p=dygraphs.git diff --git a/dygraph-utils.js b/dygraph-utils.js index bf488ef..1d1623f 100644 --- a/dygraph-utils.js +++ b/dygraph-utils.js @@ -11,13 +11,21 @@ * search) and generic DOM-manipulation functions. */ +/*jshint globalstrict: true */ +/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */ +"use strict"; + Dygraph.LOG_SCALE = 10; Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); -/** @private */ +/** + * @private + * @param {number} x + * @return {number} + */ Dygraph.log10 = function(x) { return Math.log(x) / Dygraph.LN_TEN; -} +}; // Various logging levels. Dygraph.DEBUG = 1; @@ -25,55 +33,110 @@ Dygraph.INFO = 2; Dygraph.WARNING = 3; Dygraph.ERROR = 3; -// TODO(danvk): any way I can get the line numbers to be this.warn call? +// Set this to log stack traces on warnings, etc. +// This requires stacktrace.js, which is up to you to provide. +// A copy can be found in the dygraphs repo, or at +// https://github.com/eriwen/javascript-stacktrace +Dygraph.LOG_STACK_TRACES = false; + +/** A dotted line stroke pattern. */ +Dygraph.DOTTED_LINE = [2, 2]; +/** A dashed line stroke pattern. */ +Dygraph.DASHED_LINE = [7, 3]; +/** A dot dash stroke pattern. */ +Dygraph.DOT_DASH_LINE = [7, 2, 2, 2]; + /** - * @private * Log an error on the JS console at the given severity. - * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR} - * @param { String } The message to log. + * @param {number} severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR} + * @param {string} message The message to log. + * @private */ Dygraph.log = function(severity, message) { - if (typeof(console) != 'undefined') { + var st; + if (typeof(printStackTrace) != 'undefined') { + try { + // Remove uninteresting bits: logging functions and paths. + st = printStackTrace({guess:false}); + while (st[0].indexOf("stacktrace") != -1) { + st.splice(0, 1); + } + + st.splice(0, 2); + for (var i = 0; i < st.length; i++) { + st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1') + .replace(/\@.*\/([^\/]*)/, '@$1') + .replace('[object Object].', ''); + } + var top_msg = st.splice(0, 1)[0]; + message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')'; + } catch(e) { + // Oh well, it was worth a shot! + } + } + + if (typeof(window.console) != 'undefined') { switch (severity) { case Dygraph.DEBUG: - console.debug('dygraphs: ' + message); + window.console.debug('dygraphs: ' + message); break; case Dygraph.INFO: - console.info('dygraphs: ' + message); + window.console.info('dygraphs: ' + message); break; case Dygraph.WARNING: - console.warn('dygraphs: ' + message); + window.console.warn('dygraphs: ' + message); break; case Dygraph.ERROR: - console.error('dygraphs: ' + message); + window.console.error('dygraphs: ' + message); break; } } + + if (Dygraph.LOG_STACK_TRACES) { + window.console.log(st.join('\n')); + } }; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.info = function(message) { Dygraph.log(Dygraph.INFO, message); }; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.prototype.info = Dygraph.info; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.warn = function(message) { Dygraph.log(Dygraph.WARNING, message); }; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.prototype.warn = Dygraph.warn; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.error = function(message) { Dygraph.log(Dygraph.ERROR, message); }; -/** @private */ +/** + * @param {string} message + * @private + */ Dygraph.prototype.error = Dygraph.error; /** - * @private * Return the 2d context for a dygraph canvas. * * This method is only exposed for the sake of replacing the function in @@ -84,19 +147,22 @@ Dygraph.prototype.error = Dygraph.error; * var realContext = oldFunc(canvas); * return new Proxy(realContext); * }; + * @param {!HTMLCanvasElement} canvas + * @return {!CanvasRenderingContext2D} + * @private */ Dygraph.getContext = function(canvas) { - return canvas.getContext("2d"); + return /** @type{!CanvasRenderingContext2D}*/(canvas.getContext("2d")); }; /** - * @private * Add an event handler. This smooths a difference between IE and the rest of * the world. - * @param { DOM element } elem The element to add the event to. - * @param { String } type The type of the event, e.g. 'click' or 'mousemove'. - * @param { Function } fn The function to call on the event. The function takes - * one parameter: the event object. + * @param { !Element } elem The element to add the event to. + * @param { string } type The type of the event, e.g. 'click' or 'mousemove'. + * @param { function(Event):(boolean|undefined) } fn The function to call + * on the event. The function takes one parameter: the event object. + * @private */ Dygraph.addEvent = function addEvent(elem, type, fn) { if (elem.addEventListener) { @@ -108,30 +174,50 @@ Dygraph.addEvent = function addEvent(elem, type, fn) { }; /** + * Add an event handler. This event handler is kept until the graph is + * destroyed with a call to graph.destroy(). + * + * @param { !Element } elem The element to add the event to. + * @param { string } type The type of the event, e.g. 'click' or 'mousemove'. + * @param { function(Event):(boolean|undefined) } fn The function to call + * on the event. The function takes one parameter: the event object. + * @private + */ +Dygraph.prototype.addEvent = function addEvent(elem, type, fn) { + Dygraph.addEvent(elem, type, fn); + this.registeredEvents_.push({ elem : elem, type : type, fn : fn }); +}; + +/** + * Remove an event handler. This smooths a difference between IE and the rest + * of the world. + * @param {!Element} elem The element to add the event to. + * @param {string} type The type of the event, e.g. 'click' or 'mousemove'. + * @param {function(Event):(boolean|undefined)} fn The function to call + * on the event. The function takes one parameter: the event object. * @private - * Remove an event handler. This smooths a difference between IE and the rest of - * the world. - * @param { DOM element } elem The element to add the event to. - * @param { String } type The type of the event, e.g. 'click' or 'mousemove'. - * @param { Function } fn The function to call on the event. The function takes - * one parameter: the event object. */ Dygraph.removeEvent = function addEvent(elem, type, fn) { if (elem.removeEventListener) { elem.removeEventListener(type, fn, false); } else { - elem.detachEvent('on'+type, elem[type+fn]); + try { + elem.detachEvent('on'+type, elem[type+fn]); + } catch(e) { + // We only detach event listeners on a "best effort" basis in IE. See: + // http://stackoverflow.com/questions/2553632/detachevent-not-working-with-named-inline-functions + } elem[type+fn] = null; } }; /** - * @private * Cancels further processing of an event. This is useful to prevent default * browser actions, e.g. highlighting text on a double-click. * Based on the article at * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel - * @param { Event } e The event whose normal behavior should be canceled. + * @param { !Event } e The event whose normal behavior should be canceled. + * @private */ Dygraph.cancelEvent = function(e) { e = e ? e : window.event; @@ -151,10 +237,10 @@ Dygraph.cancelEvent = function(e) { * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This * is used to generate default series colors which are evenly spaced on the * color wheel. - * @param { Number } hue Range is 0.0-1.0. - * @param { Number } saturation Range is 0.0-1.0. - * @param { Number } value Range is 0.0-1.0. - * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255. + * @param { number } hue Range is 0.0-1.0. + * @param { number } saturation Range is 0.0-1.0. + * @param { number } value Range is 0.0-1.0. + * @return { string } "rgb(r,g,b)" where r, g and b range from 0-255. * @private */ Dygraph.hsvToRGB = function (hue, saturation, value) { @@ -195,6 +281,9 @@ Dygraph.hsvToRGB = function (hue, saturation, value) { /** * Find the x-coordinate of the supplied object relative to the left side * of the page. + * TODO(danvk): change obj type from Node -> !Node + * @param {Node} obj + * @return {number} * @private */ Dygraph.findPosX = function(obj) { @@ -222,6 +311,10 @@ Dygraph.findPosX = function(obj) { /** * Find the y-coordinate of the supplied object relative to the top of the * page. + * TODO(danvk): change obj type from Node -> !Node + * TODO(danvk): consolidate with findPosX and return an {x, y} object. + * @param {Node} obj + * @return {number} * @private */ Dygraph.findPosY = function(obj) { @@ -247,16 +340,18 @@ Dygraph.findPosY = function(obj) { }; /** - * @private * Returns the x-coordinate of the event in a coordinate system where the * top-left corner of the page (not the window) is (0,0). * Taken from MochiKit.Signal + * @param {!Event} e + * @return {number} + * @private */ Dygraph.pageX = function(e) { if (e.pageX) { return (!e.pageX || e.pageX < 0) ? 0 : e.pageX; } else { - var de = document; + var de = document.documentElement; var b = document.body; return e.clientX + (de.scrollLeft || b.scrollLeft) - @@ -265,16 +360,18 @@ Dygraph.pageX = function(e) { }; /** - * @private * Returns the y-coordinate of the event in a coordinate system where the * top-left corner of the page (not the window) is (0,0). * Taken from MochiKit.Signal + * @param {!Event} e + * @return {number} + * @private */ Dygraph.pageY = function(e) { if (e.pageY) { return (!e.pageY || e.pageY < 0) ? 0 : e.pageY; } else { - var de = document; + var de = document.documentElement; var b = document.body; return e.clientY + (de.scrollTop || b.scrollTop) - @@ -283,13 +380,31 @@ Dygraph.pageY = function(e) { }; /** + * This returns true unless the parameter is 0, null, undefined or NaN. + * TODO(danvk): rename this function to something like 'isNonZeroNan'. + * + * @param {number} x The number to consider. + * @return {boolean} Whether the number is zero or NaN. * @private - * @param { Number } x The number to consider. - * @return { Boolean } Whether the number is zero or NaN. */ -// TODO(danvk): rename this function to something like 'isNonZeroNan'. Dygraph.isOK = function(x) { - return x && !isNaN(x); + return !!x && !isNaN(x); +}; + +/** + * @param { {x:?number,y:?number,yval:?number} } p The point to consider, valid + * points are {x, y} objects + * @param { boolean } allowNaNY Treat point with y=NaN as valid + * @return { boolean } Whether the point has numeric x and y. + * @private + */ +Dygraph.isValidPoint = function(p, allowNaNY) { + if (!p) return false; // null or undefined object + if (p.yval === null) return false; // missing point + if (p.x === null || p.x === undefined) return false; + if (p.y === null || p.y === undefined) return false; + if (isNaN(p.x) || (!allowNaNY && isNaN(p.y))) return false; + return true; }; /** @@ -305,9 +420,9 @@ Dygraph.isOK = function(x) { * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for * output examples. * - * @param {Number} x The number to format - * @param {Number} opt_precision The precision to use, default 2. - * @return {String} A string formatted like %g in printf. The max generated + * @param {number} x The number to format + * @param {number=} opt_precision The precision to use, default 2. + * @return {string} A string formatted like %g in printf. The max generated * string length should be precision + 6 (e.g 1.123e+300). */ Dygraph.floatFormat = function(x, opt_precision) { @@ -330,13 +445,15 @@ Dygraph.floatFormat = function(x, opt_precision) { // // Finally, the argument for toExponential() is the number of trailing digits, // so we take off 1 for the value before the '.'. - return (Math.abs(x) < 1.0e-3 && x != 0.0) ? + return (Math.abs(x) < 1.0e-3 && x !== 0.0) ? x.toExponential(p - 1) : x.toPrecision(p); }; /** - * @private * Converts '9' to '09' (useful for dates) + * @param {number} x + * @return {string} + * @private */ Dygraph.zeropad = function(x) { if (x < 10) return "0" + x; else return "" + x; @@ -344,8 +461,9 @@ Dygraph.zeropad = function(x) { /** * Return a string version of the hours, minutes and seconds portion of a date. - * @param {Number} date The JavaScript date (ms since epoch) - * @return {String} A time of the form "HH:MM:SS" + * + * @param {number} date The JavaScript date (ms since epoch) + * @return {string} A time of the form "HH:MM:SS" * @private */ Dygraph.hmsString_ = function(date) { @@ -362,9 +480,9 @@ Dygraph.hmsString_ = function(date) { /** * Round a number to the specified number of digits past the decimal point. - * @param {Number} num The number to round - * @param {Number} places The number of decimals to which to round - * @return {Number} The rounded number + * @param {number} num The number to round + * @param {number} places The number of decimals to which to round + * @return {number} The rounded number * @private */ Dygraph.round_ = function(num, places) { @@ -373,69 +491,85 @@ Dygraph.round_ = function(num, places) { }; /** - * @private * Implementation of binary search over an array. * Currently does not work when val is outside the range of arry's values. - * @param { Integer } val the value to search for - * @param { Integer[] } arry is the value over which to search - * @param { Integer } abs If abs > 0, find the lowest entry greater than val - * If abs < 0, find the highest entry less than val. - * if abs == 0, find the entry that equals val. - * @param { Integer } [low] The first index in arry to consider (optional) - * @param { Integer } [high] The last index in arry to consider (optional) + * @param {number} val the value to search for + * @param {Array.} arry is the value over which to search + * @param {number} abs If abs > 0, find the lowest entry greater than val + * If abs < 0, find the highest entry less than val. + * If abs == 0, find the entry that equals val. + * @param {number=} low The first index in arry to consider (optional) + * @param {number=} high The last index in arry to consider (optional) + * @return {number} Index of the element, or -1 if it isn't found. + * @private */ Dygraph.binarySearch = function(val, arry, abs, low, high) { - if (low == null || high == null) { + if (low === null || low === undefined || + high === null || high === undefined) { low = 0; high = arry.length - 1; } if (low > high) { return -1; } - if (abs == null) { + if (abs === null || abs === undefined) { abs = 0; } var validIndex = function(idx) { return idx >= 0 && idx < arry.length; - } - var mid = parseInt((low + high) / 2); + }; + var mid = parseInt((low + high) / 2, 10); var element = arry[mid]; + var idx; if (element == val) { return mid; - } - if (element > val) { + } else if (element > val) { if (abs > 0) { // Accept if element > val, but also if prior element < val. - var idx = mid - 1; + idx = mid - 1; if (validIndex(idx) && arry[idx] < val) { return mid; } } return Dygraph.binarySearch(val, arry, abs, low, mid - 1); - } - if (element < val) { + } else if (element < val) { if (abs < 0) { // Accept if element < val, but also if prior element > val. - var idx = mid + 1; + idx = mid + 1; if (validIndex(idx) && arry[idx] > val) { return mid; } } return Dygraph.binarySearch(val, arry, abs, mid + 1, high); } + return -1; // can't actually happen, but makes closure compiler happy }; /** - * @private * Parses a date, returning the number of milliseconds since epoch. This can be * passed in as an xValueParser in the Dygraph constructor. * TODO(danvk): enumerate formats that this understands. - * @param {String} A date in YYYYMMDD format. - * @return {Number} Milliseconds since epoch. + * + * @param {string} dateStr A date in a variety of possible string formats. + * @return {number} Milliseconds since epoch. + * @private */ Dygraph.dateParser = function(dateStr) { var dateStrSlashed; var d; + + // Let the system try the format first, with one caveat: + // YYYY-MM-DD[ HH:MM:SS] is interpreted as UTC by a variety of browsers. + // dygraphs displays dates in local time, so this will result in surprising + // inconsistencies. But if you specify "T" or "Z" (i.e. YYYY-MM-DDTHH:MM:SS), + // then you probably know what you're doing, so we'll let you go ahead. + // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255 + if (dateStr.search("-") == -1 || + dateStr.search("T") != -1 || dateStr.search("Z") != -1) { + d = Dygraph.dateStrToMillis(dateStr); + if (d && !isNaN(d)) return d; + } + if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12' dateStrSlashed = dateStr.replace("-", "/", "g"); while (dateStrSlashed.search("-") != -1) { @@ -444,8 +578,8 @@ Dygraph.dateParser = function(dateStr) { d = Dygraph.dateStrToMillis(dateStrSlashed); } else if (dateStr.length == 8) { // e.g. '20090712' // TODO(danvk): remove support for this format. It's confusing. - dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) - + "/" + dateStr.substr(6,2); + dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" + + dateStr.substr(6,2); d = Dygraph.dateStrToMillis(dateStrSlashed); } else { // Any format that Date.parse will accept, e.g. "2009/07/12" or @@ -460,12 +594,12 @@ Dygraph.dateParser = function(dateStr) { }; /** - * @private * This is identical to JavaScript's built-in Date.parse() method, except that * it doesn't get replaced with an incompatible method by aggressive JS * libraries like MooTools or Joomla. - * @param { String } str The date string, e.g. "2011/05/06" - * @return { Integer } millis since epoch + * @param {string} str The date string, e.g. "2011/05/06" + * @return {number} millis since epoch + * @private */ Dygraph.dateStrToMillis = function(str) { return new Date(str).getTime(); @@ -475,9 +609,12 @@ Dygraph.dateStrToMillis = function(str) { /** * Copies all the properties from o to self. * + * @param {!Object} self + * @param {!Object} o + * @return {!Object} * @private */ -Dygraph.update = function (self, o) { +Dygraph.update = function(self, o) { if (typeof(o) != 'undefined' && o !== null) { for (var k in o) { if (o.hasOwnProperty(k)) { @@ -491,6 +628,9 @@ Dygraph.update = function (self, o) { /** * Copies all the properties from o to self. * + * @param {!Object} self + * @param {!Object} o + * @return {!Object} * @private */ Dygraph.updateDeep = function (self, o) { @@ -505,7 +645,7 @@ Dygraph.updateDeep = function (self, o) { if (typeof(o) != 'undefined' && o !== null) { for (var k in o) { if (o.hasOwnProperty(k)) { - if (o[k] == null) { + if (o[k] === null) { self[k] = null; } else if (Dygraph.isArrayLike(o[k])) { self[k] = o[k].slice(); @@ -513,7 +653,7 @@ Dygraph.updateDeep = function (self, o) { // DOM objects are shallowly-copied. self[k] = o[k]; } else if (typeof(o[k]) == 'object') { - if (typeof(self[k]) != 'object') { + if (typeof(self[k]) != 'object' || self[k] === null) { self[k] = {}; } Dygraph.updateDeep(self[k], o[k]); @@ -527,9 +667,11 @@ Dygraph.updateDeep = function (self, o) { }; /** + * @param {Object} o + * @return {boolean} * @private */ -Dygraph.isArrayLike = function (o) { +Dygraph.isArrayLike = function(o) { var typ = typeof(o); if ( (typ != 'object' && !(typ == 'function' && @@ -544,6 +686,8 @@ Dygraph.isArrayLike = function (o) { }; /** + * @param {Object} o + * @return {boolean} * @private */ Dygraph.isDateLike = function (o) { @@ -556,6 +700,8 @@ Dygraph.isDateLike = function (o) { /** * Note: this only seems to work for arrays. + * @param {!Array} o + * @return {!Array} * @private */ Dygraph.clone = function(o) { @@ -572,27 +718,177 @@ Dygraph.clone = function(o) { }; /** - * @private * Create a new canvas element. This is more complex than a simple * document.createElement("canvas") because of IE and excanvas. + * + * @return {!HTMLCanvasElement} + * @private */ Dygraph.createCanvas = function() { var canvas = document.createElement("canvas"); - isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); + var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) { - canvas = G_vmlCanvasManager.initElement(canvas); + canvas = G_vmlCanvasManager.initElement( + /**@type{!HTMLCanvasElement}*/(canvas)); } return canvas; }; /** + * Checks whether the user is on an Android browser. + * Android does not fully support the tag, e.g. w/r/t/ clipping. + * @return {boolean} + * @private + */ +Dygraph.isAndroid = function() { + return (/Android/).test(navigator.userAgent); +}; + + +/** + * TODO(danvk): use @template here when it's better supported for classes. + * @param {!Array} array + * @param {number} start + * @param {number} length + * @param {function(!Array,?):boolean=} predicate + * @constructor + */ +Dygraph.Iterator = function(array, start, length, predicate) { + start = start || 0; + length = length || array.length; + this.hasNext = true; // Use to identify if there's another element. + this.peek = null; // Use for look-ahead + this.start_ = start; + this.array_ = array; + this.predicate_ = predicate; + this.end_ = Math.min(array.length, start + length); + this.nextIdx_ = start - 1; // use -1 so initial advance works. + this.next(); // ignoring result. +}; + +/** + * @return {Object} + */ +Dygraph.Iterator.prototype.next = function() { + if (!this.hasNext) { + return null; + } + var obj = this.peek; + + var nextIdx = this.nextIdx_ + 1; + var found = false; + while (nextIdx < this.end_) { + if (!this.predicate_ || this.predicate_(this.array_, nextIdx)) { + this.peek = this.array_[nextIdx]; + found = true; + break; + } + nextIdx++; + } + this.nextIdx_ = nextIdx; + if (!found) { + this.hasNext = false; + this.peek = null; + } + return obj; +}; + +/** + * Returns a new iterator over array, between indexes start and + * start + length, and only returns entries that pass the accept function + * + * @param {!Array} array the array to iterate over. + * @param {number} start the first index to iterate over, 0 if absent. + * @param {number} length the number of elements in the array to iterate over. + * This, along with start, defines a slice of the array, and so length + * doesn't imply the number of elements in the iterator when accept doesn't + * always accept all values. array.length when absent. + * @param {function(?):boolean=} opt_predicate a function that takes + * parameters array and idx, which returns true when the element should be + * returned. If omitted, all elements are accepted. * @private + */ +Dygraph.createIterator = function(array, start, length, opt_predicate) { + return new Dygraph.Iterator(array, start, length, opt_predicate); +}; + +// Shim layer with setTimeout fallback. +// From: http://paulirish.com/2011/requestanimationframe-for-smart-animating/ +// Should be called with the window context: +// Dygraph.requestAnimFrame.call(window, function() {}) +Dygraph.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; +})(); + +/** + * Call a function at most maxFrames times at an attempted interval of + * framePeriodInMillis, then call a cleanup function once. repeatFn is called + * once immediately, then at most (maxFrames - 1) times asynchronously. If + * maxFrames==1, then cleanup_fn() is also called synchronously. This function + * is used to sequence animation. + * @param {function(number)} repeatFn Called repeatedly -- takes the frame + * number (from 0 to maxFrames-1) as an argument. + * @param {number} maxFrames The max number of times to call repeatFn + * @param {number} framePeriodInMillis Max requested time between frames. + * @param {function()} cleanupFn A function to call after all repeatFn calls. + * @private + */ +Dygraph.repeatAndCleanup = function(repeatFn, maxFrames, framePeriodInMillis, + cleanupFn) { + var frameNumber = 0; + var previousFrameNumber; + var startTime = new Date().getTime(); + repeatFn(frameNumber); + if (maxFrames == 1) { + cleanupFn(); + return; + } + var maxFrameArg = maxFrames - 1; + + (function loop() { + if (frameNumber >= maxFrames) return; + Dygraph.requestAnimFrame.call(window, function() { + // Determine which frame to draw based on the delay so far. Will skip + // frames if necessary. + var currentTime = new Date().getTime(); + var delayInMillis = currentTime - startTime; + previousFrameNumber = frameNumber; + frameNumber = Math.floor(delayInMillis / framePeriodInMillis); + var frameDelta = frameNumber - previousFrameNumber; + // If we predict that the subsequent repeatFn call will overshoot our + // total frame target, so our last call will cause a stutter, then jump to + // the last call immediately. If we're going to cause a stutter, better + // to do it faster than slower. + var predictOvershootStutter = (frameNumber + frameDelta) > maxFrameArg; + if (predictOvershootStutter || (frameNumber >= maxFrameArg)) { + repeatFn(maxFrameArg); // Ensure final call with maxFrameArg. + cleanupFn(); + } else { + if (frameDelta !== 0) { // Don't call repeatFn with duplicate frames. + repeatFn(frameNumber); + } + loop(); + } + }); + })(); +}; + +/** * This function will scan the option list and determine if they * require us to recalculate the pixel positions of each point. - * @param { List } a list of options to check. - * @return { Boolean } true if the graph needs new points else false. + * @param {!Array.} labels a list of options to check. + * @param {!Object} attrs + * @return {boolean} true if the graph needs new points else false. + * @private */ Dygraph.isPixelChangingOptionList = function(labels, attrs) { // A whitelist of options that do not change pixel positions. @@ -605,13 +901,11 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { 'axisLineColor': true, 'axisLineWidth': true, 'clickCallback': true, - 'colorSaturation': true, - 'colorValue': true, - 'colors': true, - 'connectSeparatedPoints': true, 'digitsAfterDecimal': true, 'drawCallback': true, + 'drawHighlightPointCallback': true, 'drawPoints': true, + 'drawPointCallback': true, 'drawXGrid': true, 'drawYGrid': true, 'fillAlpha': true, @@ -665,7 +959,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { } // Iterate through the list of updated options. - for (property in attrs) { + for (var property in attrs) { // Break early if we already know we need new points from a previous option. if (requiresNewPoints) { break; @@ -675,7 +969,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { if (seriesNamesDictionary[property]) { // This property value is a list of options for this series. // If any of these sub properties are not pixel safe, set the flag. - for (subProperty in attrs[property]) { + for (var subProperty in attrs[property]) { // Break early if we already know we need new points from a previous option. if (requiresNewPoints) { break; @@ -693,3 +987,228 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { return requiresNewPoints; }; + +/** + * Compares two arrays to see if they are equal. If either parameter is not an + * array it will return false. Does a shallow compare + * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false. + * @param {!Array.} array1 first array + * @param {!Array.} array2 second array + * @return {boolean} True if both parameters are arrays, and contents are equal. + * @template T + */ +Dygraph.compareArrays = function(array1, array2) { + if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + for (var i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +}; + +/** + * @param {!CanvasRenderingContext2D} ctx the canvas context + * @param {number} sides the number of sides in the shape. + * @param {number} radius the radius of the image. + * @param {number} cx center x coordate + * @param {number} cy center y coordinate + * @param {number=} rotationRadians the shift of the initial angle, in radians. + * @param {number=} delta the angle shift for each line. If missing, creates a + * regular polygon. + * @private + */ +Dygraph.regularShape_ = function( + ctx, sides, radius, cx, cy, rotationRadians, delta) { + rotationRadians = rotationRadians || 0; + delta = delta || Math.PI * 2 / sides; + + ctx.beginPath(); + var initialAngle = rotationRadians; + var angle = initialAngle; + + var computeCoordinates = function() { + var x = cx + (Math.sin(angle) * radius); + var y = cy + (-Math.cos(angle) * radius); + return [x, y]; + }; + + var initialCoordinates = computeCoordinates(); + var x = initialCoordinates[0]; + var y = initialCoordinates[1]; + ctx.moveTo(x, y); + + for (var idx = 0; idx < sides; idx++) { + angle = (idx == sides - 1) ? initialAngle : (angle + delta); + var coords = computeCoordinates(); + ctx.lineTo(coords[0], coords[1]); + } + ctx.fill(); + ctx.stroke(); +}; + +/** + * TODO(danvk): be more specific on the return type. + * @param {number} sides + * @param {number=} rotationRadians + * @param {number=} delta + * @return {Function} + * @private + */ +Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) { + return function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + ctx.fillStyle = "white"; + Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta); + }; +}; + +Dygraph.Circles = { + DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) { + ctx.beginPath(); + ctx.fillStyle = color; + ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false); + ctx.fill(); + }, + TRIANGLE : Dygraph.shapeFunction_(3), + SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4), + DIAMOND : Dygraph.shapeFunction_(4), + PENTAGON : Dygraph.shapeFunction_(5), + HEXAGON : Dygraph.shapeFunction_(6), + CIRCLE : function(g, name, ctx, cx, cy, color, radius) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.fillStyle = "white"; + ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + }, + STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5), + PLUS : function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy); + ctx.lineTo(cx - radius, cy); + ctx.closePath(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(cx, cy + radius); + ctx.lineTo(cx, cy - radius); + ctx.closePath(); + ctx.stroke(); + }, + EX : function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy + radius); + ctx.lineTo(cx - radius, cy - radius); + ctx.closePath(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy - radius); + ctx.lineTo(cx - radius, cy + radius); + ctx.closePath(); + ctx.stroke(); + } +}; + +/** + * To create a "drag" interaction, you typically register a mousedown event + * handler on the element where the drag begins. In that handler, you register a + * mouseup handler on the window to determine when the mouse is released, + * wherever that release happens. This works well, except when the user releases + * the mouse over an off-domain iframe. In that case, the mouseup event is + * handled by the iframe and never bubbles up to the window handler. + * + * To deal with this issue, we cover iframes with high z-index divs to make sure + * they don't capture mouseup. + * + * Usage: + * element.addEventListener('mousedown', function() { + * var tarper = new Dygraph.IFrameTarp(); + * tarper.cover(); + * var mouseUpHandler = function() { + * ... + * window.removeEventListener(mouseUpHandler); + * tarper.uncover(); + * }; + * window.addEventListener('mouseup', mouseUpHandler); + * }; + * + * @constructor + */ +Dygraph.IFrameTarp = function() { + /** @type {Array.} */ + this.tarps = []; +}; + +/** + * Find all the iframes in the document and cover them with high z-index + * transparent divs. + */ +Dygraph.IFrameTarp.prototype.cover = function() { + var iframes = document.getElementsByTagName("iframe"); + for (var i = 0; i < iframes.length; i++) { + var iframe = iframes[i]; + var x = Dygraph.findPosX(iframe), + y = Dygraph.findPosY(iframe), + width = iframe.offsetWidth, + height = iframe.offsetHeight; + + var div = document.createElement("div"); + div.style.position = "absolute"; + div.style.left = x + 'px'; + div.style.top = y + 'px'; + div.style.width = width + 'px'; + div.style.height = height + 'px'; + div.style.zIndex = 999; + document.body.appendChild(div); + this.tarps.push(div); + } +}; + +/** + * Remove all the iframe covers. You should call this in a mouseup handler. + */ +Dygraph.IFrameTarp.prototype.uncover = function() { + for (var i = 0; i < this.tarps.length; i++) { + this.tarps[i].parentNode.removeChild(this.tarps[i]); + } + this.tarps = []; +}; + +/** + * Determine whether |data| is delimited by CR, CRLF, LF, LFCR. + * @param {string} data + * @return {?string} the delimiter that was detected (or null on failure). + */ +Dygraph.detectLineDelimiter = function(data) { + for (var i = 0; i < data.length; i++) { + var code = data.charAt(i); + if (code === '\r') { + // Might actually be "\r\n". + if (((i + 1) < data.length) && (data.charAt(i + 1) === '\n')) { + return '\r\n'; + } + return code; + } + if (code === '\n') { + // Might actually be "\n\r". + if (((i + 1) < data.length) && (data.charAt(i + 1) === '\r')) { + return '\n\r'; + } + return code; + } + } + + return null; +};