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