Synchronization tool synchronizer
authorDan Vanderkam <danvdk@gmail.com>
Sat, 29 Nov 2014 05:47:16 +0000 (00:47 -0500)
committerDan Vanderkam <danvdk@gmail.com>
Sat, 29 Nov 2014 05:51:51 +0000 (00:51 -0500)
dygraph.js
extras/synchronizer.js [new file with mode: 0644]
tests/synchronize.html

index 02be031..fe56d3d 100644 (file)
@@ -2153,7 +2153,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
  * legend. The selection can be cleared using clearSelection() and queried
  * using getSelection().
  * @param {number} row Row number that should be highlighted (i.e. appear with
- * hover dots on the chart). Set to false to clear any selection.
+ * hover dots on the chart).
  * @param {seriesName} optional series name to highlight that series with the
  * the highlightSeriesOpts setting.
  * @param { locked } optional If true, keep seriesName selected when mousing
diff --git a/extras/synchronizer.js b/extras/synchronizer.js
new file mode 100644 (file)
index 0000000..66dc2d8
--- /dev/null
@@ -0,0 +1,201 @@
+/**
+ * Synchronize zooming and/or selections between a set of dygraphs.
+ *
+ * Usage:
+ *
+ *   var g1 = new Dygraph(...),
+ *       g2 = new Dygraph(...),
+ *       ...;
+ *   var sync = Dygraph.synchronize(g1, g2, ...);
+ *   // charts are now synchronized
+ *   sync.detach();
+ *   // charts are no longer synchronized
+ *
+ * You can set options using the last parameter, for example:
+ *
+ *   var sync = Dygraph.synchronize(g1, g2, g3, {
+ *      selection: true,
+ *      zoom: true
+ *   });
+ *
+ * The default is to synchronize both of these.
+ *
+ * Instead of passing one Dygraph objet as each parameter, you may also pass an
+ * array of dygraphs:
+ *
+ *   var sync = Dygraph.synchronize([g1, g2, g3], {
+ *      selection: false,
+ *      zoom: true
+ *   });
+ */
+(function() {
+/* global Dygraph:false */
+'use strict';
+
+Dygraph.synchronize = function(/* dygraphs..., opts */) {
+  if (arguments.length === 0) {
+    throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
+  }
+
+  var OPTIONS = ['selection', 'zoom'];
+  var opts = {
+    selection: true,
+    zoom: true
+  };
+  var dygraphs = [];
+
+  var parseOpts = function(obj) {
+    if (!(obj instanceof Object)) {
+      throw 'Last argument must be either Dygraph or Object.';
+    } else {
+      for (var i = 0; i < OPTIONS.length; i++) {
+        var optName = OPTIONS[i];
+        if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
+      }
+    }
+  };
+
+  if (arguments[0] instanceof Dygraph) {
+    // Arguments are Dygraph objects.
+    for (var i = 0; i < arguments.length; i++) {
+      if (arguments[i] instanceof Dygraph) {
+        dygraphs.push(arguments[i]);
+      } else {
+        break;
+      }
+    }
+    if (i < arguments.length - 1) {
+      throw 'Invalid invocation of Dygraph.synchronize(). ' +
+            'All but the last argument must be Dygraph objects.';
+    } else if (i == arguments.length - 1) {
+      parseOpts(arguments[arguments.length - 1]);
+    }
+  } else if (arguments[0].length) {
+    // Invoked w/ list of dygraphs, options
+    for (var i = 0; i < arguments[0].length; i++) {
+      dygraphs.push(arguments[0][i]);
+    }
+    if (arguments.length == 2) {
+      parseOpts(arguments[1]);
+    } else if (arguments.length > 2) {
+      throw 'Invalid invocation of Dygraph.synchronize(). ' +
+            'Expected two arguments: array and optional options argument.';
+    }  // otherwise arguments.length == 1, which is fine.
+  } else {
+    throw 'Invalid invocation of Dygraph.synchronize(). ' +
+          'First parameter must be either Dygraph or list of Dygraphs.';
+  }
+
+  if (dygraphs.length < 2) {
+    throw 'Invalid invocation of Dygraph.synchronize(). ' +
+          'Need two or more dygraphs to synchronize.';
+  }
+
+  // Listen for draw, highlight, unhighlight callbacks.
+  if (opts.zoom) {
+    attachZoomHandlers(dygraphs);
+  }
+
+  if (opts.selection) {
+    attachSelectionHandlers(dygraphs);
+  }
+
+  return {
+    detach: function() {
+      for (var i = 0; i < dygraphs.length; i++) {
+        var g = dygraphs[i];
+        if (opts.zoom) {
+          g.updateOptions({drawCallback: null});
+        }
+        if (opts.selection) {
+          g.updateOptions({
+            highlightCallback: null,
+            unhighlightCallback: null
+          });
+        }
+      }
+      // release references & make subsequent calls throw.
+      dygraphs = null;
+      opts = null;
+    }
+  };
+};
+
+// TODO: call any `drawCallback`s that were set before this.
+function attachZoomHandlers(gs) {
+  var block = false;
+  for (var i = 0; i < gs.length; i++) {
+    var g = gs[i];
+    g.updateOptions({
+      drawCallback: function(me, initial) {
+        if (block || initial) return;
+        block = true;
+        var range = me.xAxisRange();
+        var yrange = me.yAxisRange();
+        for (var j = 0; j < gs.length; j++) {
+          if (gs[j] == me) continue;
+          gs[j].updateOptions( {
+            dateWindow: range,
+            valueRange: yrange
+          });
+        }
+        block = false;
+      }
+    }, false /* no need to redraw */);
+  }
+}
+
+function attachSelectionHandlers(gs) {
+  var block = false;
+  for (var i = 0; i < gs.length; i++) {
+    var g = gs[i];
+    g.updateOptions({
+      highlightCallback: function(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);
+          if (idx !== null) {
+            gs[i].setSelection(idx, seriesName);
+          }
+        }
+        block = false;
+      },
+      unhighlightCallback: function(event) {
+        if (block) return;
+        block = true;
+        var me = this;
+        for (var i = 0; i < gs.length; i++) {
+          if (me == gs[i]) continue;
+          gs[i].clearSelection();
+        }
+        block = false;
+      }
+    });
+  }
+}
+
+// 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;
+}
+
+})();
index c878223..b93a219 100644 (file)
     <script type="text/javascript" src="dygraph-combined.js"></script>
     -->
     <script type="text/javascript" src="../dygraph-dev.js"></script>
+    <script type="text/javascript" src="../extras/synchronizer.js"></script>
 
     <script type="text/javascript" src="data.js"></script>
     <style type="text/css">
-      #div1 { position: absolute; left: 10px; top: 30px; }
-      #div2 { position: absolute; left: 520px; top: 30px; }
-      #div3 { position: absolute; left: 10px; top: 340px; }
-      #div4 { position: absolute; left: 520px; top: 340px; }
+      .chart { width: 500px; height: 300px; }
+      .chart-container { overflow: hidden; }
+      #div1 { float: left; }
+      #div2 { float: left; }
+      #div3 { float: left; clear: left; }
+      #div4 { float: left; }
     </style>
   </head>
   <body>
     <p>Zooming and panning on any of the charts will zoom and pan all the
-    others.</p>
+    others. Selecting points on one will select points on the others.</p>
 
-    <div id="div1" style="width:500px; height:300px;"></div>
-    <div id="div2" style="width:500px; height:300px;"></div>
-    <div id="div3" style="width:500px; height:300px;"></div>
-    <div id="div4" style="width:500px; height:300px;"></div>
+    <p>To use this, source <a href="https://github.com/danvk/dygraphs/blob/master/extras/synchronizer.js"><code>extras/synchronizer.js</code></a> on your page.
+    See the comments in that file for usage.</p>
+
+    <div class="chart-container">
+      <div id="div1" class="chart"></div>
+      <div id="div2" class="chart"></div>
+      <div id="div3" class="chart"></div>
+      <div id="div4" class="chart"></div>
+    </div>
+
+    <p>
+      Synchronize what?
+      <input type=checkbox id='chk-zoom' checked onChange='update()'><label for='chk-zoom'> Zoom</label>
+      <input type=checkbox id='chk-selection' checked onChange='update()'><label for='chk-selection'> Selection</label>
+    </p>
 
     <script type="text/javascript">
       gs = [];
       var blockRedraw = false;
-      var initialized = false;
       for (var i = 1; i <= 4; i++) {
         gs.push(
           new Dygraph(
             NoisyData, {
               rollPeriod: 7,
               errorBars: true,
-              drawCallback: function(me, initial) {
-                if (blockRedraw || initial) return;
-                blockRedraw = true;
-                var range = me.xAxisRange();
-                var yrange = me.yAxisRange();
-                for (var j = 0; j < 4; j++) {
-                  if (gs[j] == me) continue;
-                  gs[j].updateOptions( {
-                    dateWindow: range,
-                    valueRange: yrange
-                  } );
-                }
-                blockRedraw = false;
-              }
             }
           )
         );
       }
+      var sync = Dygraph.synchronize(gs);
+      
+      function update() {
+        var zoom = document.getElementById('chk-zoom').checked;
+        var selection = document.getElementById('chk-selection').checked;
+        sync.detach();
+        sync = Dygraph.synchronize(gs, {
+          zoom: zoom,
+          selection: selection
+        });
+      }
     </script>
   </body>
 </html>