Copy from pr/528
[dygraphs.git] / extras / synchronizer.js
index 66dc2d8..16e4d96 100644 (file)
  *
  * The default is to synchronize both of these.
  *
- * Instead of passing one Dygraph objet as each parameter, you may also pass an
+ * Instead of passing one Dygraph object as each parameter, you may also pass an
  * array of dygraphs:
  *
  *   var sync = Dygraph.synchronize([g1, g2, g3], {
  *      selection: false,
  *      zoom: true
  *   });
+ *
+ * You may also set `range: false` if you wish to only sync the x-axis.
+ * The `range` option has no effect unless `zoom` is true (the default).
  */
 (function() {
 /* global Dygraph:false */
@@ -37,13 +40,20 @@ Dygraph.synchronize = function(/* dygraphs..., opts */) {
     throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
   }
 
-  var OPTIONS = ['selection', 'zoom'];
+  var OPTIONS = ['selection', 'zoom', 'range'];
   var opts = {
     selection: true,
-    zoom: true
+    zoom: true,
+    range: true
   };
   var dygraphs = [];
 
+  var prevCallbacks = {
+    draw: null,
+    highlight: null,
+    unhighlight: null
+  };
+
   var parseOpts = function(obj) {
     if (!(obj instanceof Object)) {
       throw 'Last argument must be either Dygraph or Object.';
@@ -90,54 +100,64 @@ Dygraph.synchronize = function(/* dygraphs..., opts */) {
     throw 'Invalid invocation of Dygraph.synchronize(). ' +
           'Need two or more dygraphs to synchronize.';
   }
+  
+  var readycount = dygraphs.length;
+  for (var i = 0; i < dygraphs.length; i++) {
+    var g = dygraphs[i];
+    g.ready( function() {
+      if (--readycount == 0) {
+        // Listen for draw, highlight, unhighlight callbacks.
+        if (opts.zoom) {
+          attachZoomHandlers(dygraphs, opts, prevCallbacks);
+        }
 
-  // Listen for draw, highlight, unhighlight callbacks.
-  if (opts.zoom) {
-    attachZoomHandlers(dygraphs);
-  }
-
-  if (opts.selection) {
-    attachSelectionHandlers(dygraphs);
+        if (opts.selection) {
+          attachSelectionHandlers(dygraphs, prevCallbacks);
+        }
+      }
+    });
   }
-
   return {
     detach: function() {
       for (var i = 0; i < dygraphs.length; i++) {
         var g = dygraphs[i];
         if (opts.zoom) {
-          g.updateOptions({drawCallback: null});
+          g.updateOptions({drawCallback: prevCallbacks.draw});
         }
         if (opts.selection) {
           g.updateOptions({
-            highlightCallback: null,
-            unhighlightCallback: null
+            highlightCallback: prevCallbacks.highlight,
+            unhighlightCallback: prevCallbacks.unhighlight
           });
         }
       }
       // release references & make subsequent calls throw.
       dygraphs = null;
       opts = null;
+      prevCallbacks = null;
     }
   };
 };
 
-// TODO: call any `drawCallback`s that were set before this.
-function attachZoomHandlers(gs) {
+function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
   var block = false;
   for (var i = 0; i < gs.length; i++) {
     var g = gs[i];
+    prevCallbacks.draw = g.getFunctionOption('drawCallback');
     g.updateOptions({
       drawCallback: function(me, initial) {
+        if (prevCallbacks.draw) prevCallbacks.draw(me, initial);
         if (block || initial) return;
         block = true;
-        var range = me.xAxisRange();
-        var yrange = me.yAxisRange();
+        var opts = {
+          dateWindow: me.xAxisRange()
+        };
+        if (syncOpts.range) opts.valueRange = me.yAxisRange();
+
         for (var j = 0; j < gs.length; j++) {
           if (gs[j] == me) continue;
-          gs[j].updateOptions( {
-            dateWindow: range,
-            valueRange: yrange
-          });
+          gs[j].updateOptions(opts);
         }
         block = false;
       }
@@ -145,18 +165,23 @@ function attachZoomHandlers(gs) {
   }
 }
 
-function attachSelectionHandlers(gs) {
+function attachSelectionHandlers(gs, prevCallbacks) {
   var block = false;
   for (var i = 0; i < gs.length; i++) {
     var g = gs[i];
+    prevCallbacks.highlight = g.getFunctionOption('highlightCallback');
+    prevCallbacks.unhighlight = g.getFunctionOption('unhighlightCallback');
     g.updateOptions({
       highlightCallback: function(event, x, points, row, seriesName) {
+        if (prevCallbacks.highlight) {
+            prevCallbacks.highlight(event, x, points, row, seriesName);
+        }
         if (block) return;
         block = true;
         var me = this;
         for (var i = 0; i < gs.length; i++) {
           if (me == gs[i]) continue;
-          var idx = dygraphsBinarySearch(gs[i], x);
+          var idx = gs[i].getRowForX(x);
           if (idx !== null) {
             gs[i].setSelection(idx, seriesName);
           }
@@ -164,6 +189,7 @@ function attachSelectionHandlers(gs) {
         block = false;
       },
       unhighlightCallback: function(event) {
+        if (prevCallbacks.unhighlight) prevCallbacks.unhighlight(event);
         if (block) return;
         block = true;
         var me = this;
@@ -177,25 +203,4 @@ function attachSelectionHandlers(gs) {
   }
 }
 
-// Returns the index corresponding to xVal, or null if there is none.
-function dygraphsBinarySearch(g, xVal) {
-  var low = 0,
-      high = g.numRows() - 1;
-
-  while (low < high) {
-    var idx = (high + low) >> 1;
-    var x = g.getValue(idx, 0);
-    if (x < xVal) {
-      low = idx + 1;
-    } else if (x > xVal) {
-      high = idx - 1;
-    } else {
-      return idx;
-    }
-  }
-
-  // TODO: give an option to find the closest point, i.e. not demand an exact match.
-  return null;
-}
-
 })();