      1 /**
      2  * @license
      3  * Part of dygraphs, see top-level LICENSE.txt file
      4  * MIT-licenced: https://opensource.org/licenses/MIT
      5  */
      6 /**
      7  * Synchronize zooming and/or selections between a set of dygraphs.
      8  *
      9  * Usage:
     10  *
     11  *   var g1 = new Dygraph(...),
     12  *       g2 = new Dygraph(...),
     13  *       ...;
     14  *   var sync = Dygraph.synchronize(g1, g2, ...);
     15  *   // charts are now synchronized
     16  *   sync.detach();
     17  *   // charts are no longer synchronized
     18  *
     19  * You can set options using the last parameter, for example:
     20  *
     21  *   var sync = Dygraph.synchronize(g1, g2, g3, {
     22  *      selection: true,
     23  *      zoom: true
     24  *   });
     25  *
     26  * The default is to synchronize both of these.
     27  *
     28  * Instead of passing one Dygraph object as each parameter, you may also pass an
     29  * array of dygraphs:
     30  *
     31  *   var sync = Dygraph.synchronize([g1, g2, g3], {
     32  *      selection: false,
     33  *      zoom: true
     34  *   });
     35  *
     36  * You may also set `range: false` if you wish to only sync the x-axis.
     37  * The `range` option has no effect unless `zoom` is true (the default).
     38  */
     39 (function() {
     40 /* global Dygraph:false */
     41 'use strict';
     42
     43 var Dygraph;
     44 if (window.Dygraph) {
     45   Dygraph = window.Dygraph;
     46 } else if (typeof(module) !== 'undefined') {
     47   Dygraph = require('../dygraph');
     48 }
     49
     50 var synchronize = function(/* dygraphs..., opts */) {
     51   if (arguments.length === 0) {
     52     throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
     53   }
     54
     55   var OPTIONS = ['selection', 'zoom', 'range'];
     56   var opts = {
     57     selection: true,
     58     zoom: true,
     59     range: true
     60   };
     61   var dygraphs = [];
     62   var prevCallbacks = [];
     63
     64   var parseOpts = function(obj) {
     65     if (!(obj instanceof Object)) {
     66       throw 'Last argument must be either Dygraph or Object.';
     67     } else {
     68       for (var i = 0; i < OPTIONS.length; i++) {
     69         var optName = OPTIONS[i];
     70         if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
     71       }
     72     }
     73   };
     74
     75   if (arguments[0] instanceof Dygraph) {
     76     // Arguments are Dygraph objects.
     77     for (var i = 0; i < arguments.length; i++) {
     78       if (arguments[i] instanceof Dygraph) {
     79         dygraphs.push(arguments[i]);
     80       } else {
     81         break;
     82       }
     83     }
     84     if (i < arguments.length - 1) {
     85       throw 'Invalid invocation of Dygraph.synchronize(). ' +
     86             'All but the last argument must be Dygraph objects.';
     87     } else if (i == arguments.length - 1) {
     88       parseOpts(arguments[arguments.length - 1]);
     89     }
     90   } else if (arguments[0].length) {
     91     // Invoked w/ list of dygraphs, options
     92     for (var i = 0; i < arguments[0].length; i++) {
     93       dygraphs.push(arguments[0][i]);
     94     }
     95     if (arguments.length == 2) {
     96       parseOpts(arguments[1]);
     97     } else if (arguments.length > 2) {
     98       throw 'Invalid invocation of Dygraph.synchronize(). ' +
     99             'Expected two arguments: array and optional options argument.';
    100     }  // otherwise arguments.length == 1, which is fine.
    101   } else {
    102     throw 'Invalid invocation of Dygraph.synchronize(). ' +
    103           'First parameter must be either Dygraph or list of Dygraphs.';
    104   }
    105
    106   if (dygraphs.length < 2) {
    107     throw 'Invalid invocation of Dygraph.synchronize(). ' +
    108           'Need two or more dygraphs to synchronize.';
    109   }
    110
    111   var readycount = dygraphs.length;
    112   for (var i = 0; i < dygraphs.length; i++) {
    113     var g = dygraphs[i];
    114     g.ready( function() {
    115       if (--readycount == 0) {
    116         // store original callbacks
    117         var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback'];
    118         for (var j = 0; j < dygraphs.length; j++) {
    119           if (!prevCallbacks[j]) {
    120             prevCallbacks[j] = {};
    121           }
    122           for (var k = callBackTypes.length - 1; k >= 0; k--) {
    123             prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]);
    124           }
    125         }
    126
    127         // Listen for draw, highlight, unhighlight callbacks.
    128         if (opts.zoom) {
    129           attachZoomHandlers(dygraphs, opts, prevCallbacks);
    130         }
    131
    132         if (opts.selection) {
    133           attachSelectionHandlers(dygraphs, prevCallbacks);
    134         }
    135       }
    136     });
    137   }
    138
    139   return {
    140     detach: function() {
    141       for (var i = 0; i < dygraphs.length; i++) {
    142         var g = dygraphs[i];
    143         if (opts.zoom) {
    144           g.updateOptions({drawCallback: prevCallbacks[i].drawCallback});
    145         }
    146         if (opts.selection) {
    147           g.updateOptions({
    148             highlightCallback: prevCallbacks[i].highlightCallback,
    149             unhighlightCallback: prevCallbacks[i].unhighlightCallback
    150           });
    151         }
    152       }
    153       // release references & make subsequent calls throw.
    154       dygraphs = null;
    155       opts = null;
    156       prevCallbacks = null;
    157     }
    158   };
    159 };
    160
    161 function arraysAreEqual(a, b) {
    162   if (!Array.isArray(a) || !Array.isArray(b)) return false;
    163   var i = a.length;
    164   if (i !== b.length) return false;
    165   while (i--) {
    166     if (a[i] !== b[i]) return false;
    167   }
    168   return true;
    169 }
    170
    171 function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
    172   var block = false;
    173   for (var i = 0; i < gs.length; i++) {
    174     var g = gs[i];
    175     g.updateOptions({
    176       drawCallback: function(me, initial) {
    177         if (block || initial) return;
    178         block = true;
    179         var opts = {
    180           dateWindow: me.xAxisRange()
    181         };
    182         if (syncOpts.range) opts.valueRange = me.yAxisRange();
    183
    184         for (var j = 0; j < gs.length; j++) {
    185           if (gs[j] == me) {
    186             if (prevCallbacks[j] && prevCallbacks[j].drawCallback) {
    187               prevCallbacks[j].drawCallback.apply(this, arguments);
    188             }
    189             continue;
    190           }
    191
    192           // Only redraw if there are new options
    193           if (arraysAreEqual(opts.dateWindow, gs[j].getOption('dateWindow')) &&
    194               arraysAreEqual(opts.valueRange, gs[j].getOption('valueRange'))) {
    195             continue;
    196           }
    197
    198           gs[j].updateOptions(opts);
    199         }
    200         block = false;
    201       }
    202     }, true /* no need to redraw */);
    203   }
    204 }
    205
    206 function attachSelectionHandlers(gs, prevCallbacks) {
    207   var block = false;
    208   for (var i = 0; i < gs.length; i++) {
    209     var g = gs[i];
    210
    211     g.updateOptions({
    212       highlightCallback: function(event, x, points, row, seriesName) {
    213         if (block) return;
    214         block = true;
    215         var me = this;
    216         for (var i = 0; i < gs.length; i++) {
    217           if (me == gs[i]) {
    218             if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) {
    219               prevCallbacks[i].highlightCallback.apply(this, arguments);
    220             }
    221             continue;
    222           }
    223           var idx = gs[i].getRowForX(x);
    224           if (idx !== null) {
    225             gs[i].setSelection(idx, seriesName, undefined, true);
    226           }
    227         }
    228         block = false;
    229       },
    230       unhighlightCallback: function(event) {
    231         if (block) return;
    232         block = true;
    233         var me = this;
    234         for (var i = 0; i < gs.length; i++) {
    235           if (me == gs[i]) {
    236             if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) {
    237               prevCallbacks[i].unhighlightCallback.apply(this, arguments);
    238             }
    239             continue;
    240           }
    241           gs[i].clearSelection();
    242         }
    243         block = false;
    244       }
    245     }, true /* no need to redraw */);
    246   }
    247 }
    248
    249 Dygraph.synchronize = synchronize;
    250
    251 })();
    252