Add hash support.
[dygraphs.git] / dygraph-utils.js
index 7cb5019..db04314 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
@@ -90,20 +93,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;
   }
 };
 
@@ -476,6 +494,14 @@ Dygraph.update = function (self, o) {
  * @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)) {
@@ -483,6 +509,9 @@ Dygraph.updateDeep = function (self, o) {
           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] = {};
@@ -560,6 +589,43 @@ Dygraph.createCanvas = function() {
 
 /**
  * @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.
@@ -579,7 +645,6 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'colorSaturation': true,
     'colorValue': true,
     'colors': true,
-    'connectSeparatedPoints': true,
     'digitsAfterDecimal': true,
     'drawCallback': true,
     'drawPoints': true,
@@ -606,6 +671,8 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'pixelsPerYLabel': true,
     'pointClickCallback': true,
     'pointSize': true,
+    'rangeSelectorPlotFillColor': true,
+    'rangeSelectorPlotStrokeColor': true,
     'showLabelsOnHighlight': true,
     'showRoller': true,
     'sigFigs': true,
@@ -618,7 +685,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     '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.
@@ -656,7 +723,7 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
       // If this was not a series specific option list, check if its a pixel changing property.
       } else if (!pixelSafeOptions[property]) {
         requiresNewPoints = true;
-      }   
+      }
     }
   }