Enable "strict" mode -- and fix one missing "var" declaration.
[dygraphs.git] / dygraph-utils.js
index 27196d8..cebf950 100644 (file)
@@ -1,5 +1,8 @@
-// Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
-// All Rights Reserved.
+/**
+ * @license
+ * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
+ * MIT-licensed (http://opensource.org/licenses/MIT)
+ */
 
 /**
  * @fileoverview This file contains utility functions used by dygraphs. These
@@ -8,6 +11,8 @@
  * search) and generic DOM-manipulation functions.
  */
 
+"use strict";
+
 Dygraph.LOG_SCALE = 10;
 Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
 
@@ -22,7 +27,12 @@ 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;
+
 /**
  * @private
  * Log an error on the JS console at the given severity.
@@ -30,6 +40,22 @@ 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.
+    var st = printStackTrace({guess:false});
+    while (st[0].indexOf("Function.log") != 0) {
+      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].', '');
+    }
+    message += ' (' + st.splice(0, 1) + ')';
+  }
+
   if (typeof(console) != 'undefined') {
     switch (severity) {
       case Dygraph.DEBUG:
@@ -46,6 +72,10 @@ Dygraph.log = function(severity, message) {
         break;
     }
   }
+
+  if (Dygraph.LOG_STACK_TRACES) {
+    console.log(st.join('\n'));
+  }
 };
 
 /** @private */
@@ -90,20 +120,35 @@ Dygraph.getContext = function(canvas) {
  * @private
  * Add an event handler. This smooths a difference between IE and the rest of
  * the world.
- * @param { DOM element } el The element to add the event to.
- * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
+ * @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.addEvent = function(el, evt, fn) {
-  var normed_fn = function(e) {
-    if (!e) var e = window.event;
-    fn(e);
-  };
-  if (window.addEventListener) {  // Mozilla, Netscape, Firefox
-    el.addEventListener(evt, normed_fn, false);
-  } else {  // IE
-    el.attachEvent('on' + evt, normed_fn);
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+  if (elem.addEventListener) {
+    elem.addEventListener(type, fn, false);
+  } else {
+    elem[type+fn] = function(){fn(window.event);};
+    elem.attachEvent('on'+type, elem[type+fn]);
+  }
+};
+
+/**
+ * @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]);
+    elem[type+fn] = null;
   }
 };
 
@@ -172,6 +217,7 @@ Dygraph.hsvToRGB = function (hue, saturation, value) {
 // The following functions are from quirksmode.org with a modification for Safari from
 // http://blog.firetree.net/2005/07/04/javascript-find-position/
 // http://www.quirksmode.org/js/findpos.html
+// ... and modifications to support scrolling divs.
 
 /**
  * Find the x-coordinate of the supplied object relative to the left side
@@ -342,30 +388,6 @@ Dygraph.hmsString_ = function(date) {
 };
 
 /**
- * Convert a JS date (millis since epoch) to YYYY/MM/DD
- * @param {Number} date The JavaScript date (ms since epoch)
- * @return {String} A date of the form "YYYY/MM/DD"
- * @private
- */
-Dygraph.dateString_ = function(date) {
-  var zeropad = Dygraph.zeropad;
-  var d = new Date(date);
-
-  // Get the year:
-  var year = "" + d.getFullYear();
-  // Get a 0 padded month string
-  var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
-  // Get a 0 padded day string
-  var day = zeropad(d.getDate());
-
-  var ret = "";
-  var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
-  if (frac) ret = " " + Dygraph.hmsString_(date);
-
-  return year + "/" + month + "/" + day + ret;
-};
-
-/**
  * 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
@@ -494,6 +516,44 @@ Dygraph.update = function (self, o) {
 };
 
 /**
+ * Copies all the properties from o to self.
+ *
+ * @private
+ */
+Dygraph.updateDeep = function (self, o) {
+  // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
+  function isNode(o) {
+    return (
+      typeof Node === "object" ? o instanceof Node :
+      typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
+    );
+  }
+
+  if (typeof(o) != 'undefined' && o !== null) {
+    for (var k in o) {
+      if (o.hasOwnProperty(k)) {
+        if (o[k] == null) {
+          self[k] = null;
+        } else if (Dygraph.isArrayLike(o[k])) {
+          self[k] = o[k].slice();
+        } else if (isNode(o[k])) {
+          // DOM objects are shallowly-copied.
+          self[k] = o[k];
+        } else if (typeof(o[k]) == 'object') {
+          if (typeof(self[k]) != 'object') {
+            self[k] = {};
+          }
+          Dygraph.updateDeep(self[k], o[k]);
+        } else {
+          self[k] = o[k];
+        }
+      }
+    }
+  }
+  return self;
+};
+
+/**
  * @private
  */
 Dygraph.isArrayLike = function (o) {
@@ -522,6 +582,7 @@ Dygraph.isDateLike = function (o) {
 };
 
 /**
+ * Note: this only seems to work for arrays.
  * @private
  */
 Dygraph.clone = function(o) {
@@ -545,10 +606,150 @@ 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);
   }
 
   return canvas;
 };
+
+/**
+ * @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.
+ * @return { Boolean } true if the graph needs new points else false.
+ */
+Dygraph.isPixelChangingOptionList = function(labels, attrs) {
+  // 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,
+    'digitsAfterDecimal': true,
+    'drawCallback': true,
+    'drawPoints': 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,
+    'maxNumberWidth': true,
+    'panEdgeFraction': true,
+    'pixelsPerYLabel': true,
+    'pointClickCallback': true,
+    'pointSize': true,
+    'rangeSelectorPlotFillColor': true,
+    'rangeSelectorPlotStrokeColor': true,
+    'showLabelsOnHighlight': true,
+    'showRoller': true,
+    'sigFigs': true,
+    'strokeWidth': true,
+    'underlayCallback': true,
+    'unhighlightCallback': true,
+    'xAxisLabelFormatter': true,
+    'xTicker': true,
+    'xValueFormatter': true,
+    'yAxisLabelFormatter': true,
+    'yValueFormatter': true,
+    'zoomCallback': true
+  };
+
+  // Assume that we do not require new points.
+  // This will change to true if we actually do need new points.
+  var requiresNewPoints = false;
+
+  // Create a dictionary of series names for faster lookup.
+  // If there are no labels, then the dictionary stays empty.
+  var seriesNamesDictionary = { };
+  if (labels) {
+    for (var i = 1; i < labels.length; i++) {
+      seriesNamesDictionary[labels[i]] = true;
+    }
+  }
+
+  // Iterate through the list of updated options.
+  for (property in attrs) {
+    // Break early if we already know we need new points from a previous option.
+    if (requiresNewPoints) {
+      break;
+    }
+    if (attrs.hasOwnProperty(property)) {
+      // Find out of this field is actually a series specific options list.
+      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]) {
+          // Break early if we already know we need new points from a previous option.
+          if (requiresNewPoints) {
+            break;
+          }
+          if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) {
+            requiresNewPoints = true;
+          }
+        }
+      // If this was not a series specific option list, check if its a pixel changing property.
+      } else if (!pixelSafeOptions[property]) {
+        requiresNewPoints = true;
+      }
+    }
+  }
+
+  return requiresNewPoints;
+};