Update plugin registry
[dygraphs.git] / dygraph-utils.js
index bf488ef..c6c3a60 100644 (file)
  * 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 */
 Dygraph.log10 = function(x) {
   return Math.log(x) / Dygraph.LN_TEN;
-}
+};
 
 // Various logging levels.
 Dygraph.DEBUG = 1;
@@ -25,7 +29,19 @@ 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.
@@ -33,6 +49,24 @@ Dygraph.ERROR = 3;
  * @param { String } The message to log.
  */
 Dygraph.log = function(severity, message) {
+  var st;
+  if (typeof(printStackTrace) != 'undefined') {
+    // 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(/^.*@ ?/, '') + ')';
+  }
+
   if (typeof(console) != 'undefined') {
     switch (severity) {
       case Dygraph.DEBUG:
@@ -49,6 +83,10 @@ Dygraph.log = function(severity, message) {
         break;
     }
   }
+
+  if (Dygraph.LOG_STACK_TRACES) {
+    console.log(st.join('\n'));
+  }
 };
 
 /** @private */
@@ -288,11 +326,27 @@ Dygraph.pageY = function(e) {
  * @return { Boolean } Whether the number is zero or NaN.
  */
 // TODO(danvk): rename this function to something like 'isNonZeroNan'.
+// TODO(danvk): determine when else this returns false (e.g. for undefined or null)
 Dygraph.isOK = function(x) {
   return x && !isNaN(x);
 };
 
 /**
+ * @private
+ * @param { Object } 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.
+ */
+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;
+};
+
+/**
  * Number formatting function which mimicks the behavior of %g in printf, i.e.
  * either exponential or fixed format (without trailing 0s) is used depending on
  * the length of the generated string.  The advantage of this format is that
@@ -330,7 +384,7 @@ 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);
 };
 
@@ -385,28 +439,31 @@ Dygraph.round_ = function(num, places) {
  * @param { Integer } [high] The last index in arry to consider (optional)
  */
 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];
   if (element == val) {
     return mid;
   }
+
+  var idx;
   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;
       }
@@ -416,7 +473,7 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) {
   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;
       }
@@ -436,6 +493,19 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) {
 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 +514,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
@@ -505,7 +575,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();
@@ -579,7 +649,7 @@ Dygraph.clone = function(o) {
 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);
   }
@@ -589,6 +659,52 @@ Dygraph.createCanvas = function() {
 
 /**
  * @private
+ * Checks whether the user is on an Android browser.
+ * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
+ */
+Dygraph.isAndroid = function() {
+  return (/Android/).test(navigator.userAgent);
+};
+
+/**
+ * @private
+ * 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 repeat_fn {Function} Called repeatedly -- takes the number of calls
+ * (from 0 to times-1) as an argument.
+ * @param times {number} The number of times to call repeat_fn
+ * @param every_ms {number} Milliseconds between calls
+ * @param cleanup_fn {Function} A function to call after all repeat_fn 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();
+    return;
+  }
+
+  (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();
+      } else {
+        loop();
+      }
+    }, target_time - new Date().getTime());
+    // TODO(danvk): adjust every_ms to produce evenly-timed function calls.
+  })();
+};
+
+/**
+ * @private
  * 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.
@@ -605,13 +721,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 +779,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 +789,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 +807,131 @@ 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 array1 first array
+ * @param array2 second array
+ * @return True if both parameters are arrays, and contents are equal.
+ */
+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;
+};
+
+/**
+ * ctx: the canvas context
+ * sides: the number of sides in the shape.
+ * radius: the radius of the image.
+ * cx: center x coordate
+ * cy: center y coordinate
+ * rotationRadians: the shift of the initial angle, in radians.
+ * delta: the angle shift for each line. If missing, creates a regular
+ *   polygon.
+ */
+Dygraph.regularShape_ = function(
+    ctx, sides, radius, cx, cy, rotationRadians, delta) {
+  rotationRadians = rotationRadians ? rotationRadians : 0;
+  delta = delta ? delta : Math.PI * 2 / sides;
+
+  ctx.beginPath();
+  var first = true;
+  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();
+}
+
+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.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();
+    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();
+  }
+};