+ */
+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();
+ }
+ });
+ })();
+};
+
+// A whitelist of options that do not change pixel positions.
+var pixelSafeOptions = {
+ 'annotationClickHandler': true,
+ 'annotationDblClickHandler': true,
+ 'annotationMouseOutHandler': true,
+ 'annotationMouseOverHandler': true,
+ 'axisLabelColor': true,
+ 'axisLineColor': true,
+ 'axisLineWidth': true,
+ 'clickCallback': true,
+ 'drawCallback': true,
+ 'drawHighlightPointCallback': true,
+ 'drawPoints': true,
+ 'drawPointCallback': true,
+ 'drawXGrid': true,
+ 'drawYGrid': true,
+ 'fillAlpha': true,
+ 'gridLineColor': true,
+ 'gridLineWidth': true,
+ 'hideOverlayOnMouseOut': true,
+ 'highlightCallback': true,
+ 'highlightCircleSize': true,
+ 'interactionModel': true,
+ 'isZoomedIgnoreProgrammaticZoom': true,
+ 'labelsDiv': true,
+ 'labelsDivStyles': true,
+ 'labelsDivWidth': true,
+ 'labelsKMB': true,
+ 'labelsKMG2': true,
+ 'labelsSeparateLines': true,
+ 'labelsShowZeroValues': true,
+ 'legend': true,
+ 'panEdgeFraction': true,
+ 'pixelsPerYLabel': true,
+ 'pointClickCallback': true,
+ 'pointSize': true,
+ 'rangeSelectorPlotFillColor': true,
+ 'rangeSelectorPlotStrokeColor': true,
+ 'showLabelsOnHighlight': true,
+ 'showRoller': true,
+ 'strokeWidth': true,
+ 'underlayCallback': true,
+ 'unhighlightCallback': true,
+ 'zoomCallback': true
+};
+
+/**