Address code review comments
[dygraphs.git] / dygraph-utils.js
index 2a86b24..1d1623f 100644 (file)
@@ -752,7 +752,7 @@ Dygraph.isAndroid = function() {
  * @param {!Array} array
  * @param {number} start
  * @param {number} length
- * @param {function(!Array,Object):boolean=} predicate
+ * @param {function(!Array,?):boolean=} predicate
  * @constructor
  */
 Dygraph.Iterator = function(array, start, length, predicate) {
@@ -805,7 +805,7 @@ Dygraph.Iterator.prototype.next = function() {
  *     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(Object):boolean=} opt_predicate a function that takes
+ * @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
@@ -814,39 +814,71 @@ 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 N times at a given interval, then call a cleanup function
- * once. repeat_fn is called once immediately, then (times - 1) times
- * asynchronously. If times=1, then cleanup_fn() is also called synchronously.
- * @param {function(number)} repeat_fn Called repeatedly -- takes the number of
- *     calls (from 0 to times-1) as an argument.
- * @param {number} times The number of times to call repeat_fn
- * @param {number} every_ms Milliseconds between calls
- * @param {function()} cleanup_fn A function to call after all repeat_fn calls.
+ * 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(repeat_fn, times, every_ms, cleanup_fn) {
-  var count = 0;
-  var start_time = new Date().getTime();
-  repeat_fn(count);
-  if (times == 1) {
-    cleanup_fn();
+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 (count >= times) return;
-    var target_time = start_time + (1 + count) * every_ms;
-    setTimeout(function() {
-      count++;
-      repeat_fn(count);
-      if (count >= times - 1) {
-        cleanup_fn();
+    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();
       }
-    }, target_time - new Date().getTime());
-    // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
+    });
   })();
 };
 
@@ -986,18 +1018,17 @@ Dygraph.compareArrays = function(array1, array2) {
  * @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
+ * @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 ? rotationRadians : 0;
-  delta = delta ? delta : Math.PI * 2 / sides;
+  rotationRadians = rotationRadians || 0;
+  delta = delta || Math.PI * 2 / sides;
 
   ctx.beginPath();
-  var first = true;
   var initialAngle = rotationRadians;
   var angle = initialAngle;
 
@@ -1024,8 +1055,8 @@ Dygraph.regularShape_ = function(
 /**
  * TODO(danvk): be more specific on the return type.
  * @param {number} sides
- * @param {number} rotationRadians
- * @param {number} delta
+ * @param {number=} rotationRadians
+ * @param {number=} delta
  * @return {Function}
  * @private
  */
@@ -1037,21 +1068,6 @@ Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) {
   };
 };
 
-/**
- * @param {number} sides the number of sides in the shape.
- * @param {number} rotationRadians the shift of the initial angle, in radians.
- * @param {!CanvasRenderingContext2D} ctx the canvas context
- * @param {number} cx center x coordate
- * @param {number} cy center y coordinate
- * @param {string} color stroke color
- * @param {number} radius the radius of the image.
- * @param {number} delta the angle shift for each line. If missing, creates a
- *     regular polygon.
- */
-Dygraph.DrawPolygon_ = function(sides, rotationRadians, ctx, cx, cy, color, radius, delta) {
-  new Dygraph.RegularShape_(sides, rotationRadians, delta).draw(ctx, cx, cy, radius);
-};
-
 Dygraph.Circles = {
   DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
     ctx.beginPath();