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