78c8d4278f32b2050ff006d168cf61213804645a
[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  * at commit b55a71d768d2f8de62877c32b3aec9e9975ac389
37  *
38  * Copyright (c) 2009 Dan Vanderkam
39  *
40  * Permission is hereby granted, free of charge, to any person
41  * obtaining a copy of this software and associated documentation
42  * files (the "Software"), to deal in the Software without
43  * restriction, including without limitation the rights to use,
44  * copy, modify, merge, publish, distribute, sublicense, and/or sell
45  * copies of the Software, and to permit persons to whom the
46  * Software is furnished to do so, subject to the following
47  * conditions:
48  *
49  * The above copyright notice and this permission notice shall be
50  * included in all copies or substantial portions of the Software.
51  *
52  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
53  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
54  * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
55  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
56  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
57  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
58  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
59  * OTHER DEALINGS IN THE SOFTWARE.
60  */
61 (function() {
62 /* global Dygraph:false */
63 'use strict';
64
65 var Dygraph;
66 if (window.Dygraph) {
67   Dygraph = window.Dygraph;
68 } else if (typeof(module) !== 'undefined') {
69   Dygraph = require('../dygraph');
70 }
71
72 var synchronize = function(/* dygraphs..., opts */) {
73   if (arguments.length === 0) {
74     throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
75   }
76
77   var OPTIONS = ['selection', 'zoom', 'range'];
78   var opts = {
79     selection: true,
80     zoom: true,
81     range: true
82   };
83   var dygraphs = [];
84   var prevCallbacks = [];
85
86   var parseOpts = function(obj) {
87     if (!(obj instanceof Object)) {
88       throw 'Last argument must be either Dygraph or Object.';
89     } else {
90       for (var i = 0; i < OPTIONS.length; i++) {
91         var optName = OPTIONS[i];
92         if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
93       }
94     }
95   };
96
97   if (arguments[0] instanceof Dygraph) {
98     // Arguments are Dygraph objects.
99     for (var i = 0; i < arguments.length; i++) {
100       if (arguments[i] instanceof Dygraph) {
101         dygraphs.push(arguments[i]);
102       } else {
103         break;
104       }
105     }
106     if (i < arguments.length - 1) {
107       throw 'Invalid invocation of Dygraph.synchronize(). ' +
108             'All but the last argument must be Dygraph objects.';
109     } else if (i == arguments.length - 1) {
110       parseOpts(arguments[arguments.length - 1]);
111     }
112   } else if (arguments[0].length) {
113     // Invoked w/ list of dygraphs, options
114     for (var i = 0; i < arguments[0].length; i++) {
115       dygraphs.push(arguments[0][i]);
116     }
117     if (arguments.length == 2) {
118       parseOpts(arguments[1]);
119     } else if (arguments.length > 2) {
120       throw 'Invalid invocation of Dygraph.synchronize(). ' +
121             'Expected two arguments: array and optional options argument.';
122     }  // otherwise arguments.length == 1, which is fine.
123   } else {
124     throw 'Invalid invocation of Dygraph.synchronize(). ' +
125           'First parameter must be either Dygraph or list of Dygraphs.';
126   }
127
128   if (dygraphs.length < 2) {
129     throw 'Invalid invocation of Dygraph.synchronize(). ' +
130           'Need two or more dygraphs to synchronize.';
131   }
132
133   var readycount = dygraphs.length;
134   for (var i = 0; i < dygraphs.length; i++) {
135     var g = dygraphs[i];
136     g.ready( function() {
137       if (--readycount == 0) {
138         // store original callbacks
139         var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback'];
140         for (var j = 0; j < dygraphs.length; j++) {
141           if (!prevCallbacks[j]) {
142             prevCallbacks[j] = {};
143           }
144           for (var k = callBackTypes.length - 1; k >= 0; k--) {
145             prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]);
146           }
147         }
148
149         // Listen for draw, highlight, unhighlight callbacks.
150         if (opts.zoom) {
151           attachZoomHandlers(dygraphs, opts, prevCallbacks);
152         }
153
154         if (opts.selection) {
155           attachSelectionHandlers(dygraphs, prevCallbacks);
156         }
157       }
158     });
159   }
160
161   return {
162     detach: function() {
163       for (var i = 0; i < dygraphs.length; i++) {
164         var g = dygraphs[i];
165         if (opts.zoom) {
166           g.updateOptions({drawCallback: prevCallbacks[i].drawCallback});
167         }
168         if (opts.selection) {
169           g.updateOptions({
170             highlightCallback: prevCallbacks[i].highlightCallback,
171             unhighlightCallback: prevCallbacks[i].unhighlightCallback
172           });
173         }
174       }
175       // release references & make subsequent calls throw.
176       dygraphs = null;
177       opts = null;
178       prevCallbacks = null;
179     }
180   };
181 };
182
183 function arraysAreEqual(a, b) {
184   if (!Array.isArray(a) || !Array.isArray(b)) return false;
185   var i = a.length;
186   if (i !== b.length) return false;
187   while (i--) {
188     if (a[i] !== b[i]) return false;
189   }
190   return true;
191 }
192
193 function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
194   var block = false;
195   for (var i = 0; i < gs.length; i++) {
196     var g = gs[i];
197     g.updateOptions({
198       drawCallback: function(me, initial) {
199         if (block || initial) return;
200         block = true;
201         var opts = {
202           dateWindow: me.xAxisRange()
203         };
204         if (syncOpts.range) opts.valueRange = me.yAxisRange();
205
206         for (var j = 0; j < gs.length; j++) {
207           if (gs[j] == me) {
208             if (prevCallbacks[j] && prevCallbacks[j].drawCallback) {
209               prevCallbacks[j].drawCallback.apply(this, arguments);
210             }
211             continue;
212           }
213
214           // Only redraw if there are new options
215           if (arraysAreEqual(opts.dateWindow, gs[j].getOption('dateWindow')) && 
216               arraysAreEqual(opts.valueRange, gs[j].getOption('valueRange'))) {
217             continue;
218           }
219
220           gs[j].updateOptions(opts);
221         }
222         block = false;
223       }
224     }, true /* no need to redraw */);
225   }
226 }
227
228 function attachSelectionHandlers(gs, prevCallbacks) {
229   var block = false;
230   for (var i = 0; i < gs.length; i++) {
231     var g = gs[i];
232
233     g.updateOptions({
234       highlightCallback: function(event, x, points, row, seriesName) {
235         if (block) return;
236         block = true;
237         var me = this;
238         for (var i = 0; i < gs.length; i++) {
239           if (me == gs[i]) {
240             if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) {
241               prevCallbacks[i].highlightCallback.apply(this, arguments);
242             }
243             continue;
244           }
245           var idx = gs[i].getRowForX(x);
246           if (idx !== null) {
247             gs[i].setSelection(idx, seriesName);
248           }
249         }
250         block = false;
251       },
252       unhighlightCallback: function(event) {
253         if (block) return;
254         block = true;
255         var me = this;
256         for (var i = 0; i < gs.length; i++) {
257           if (me == gs[i]) {
258             if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) {
259               prevCallbacks[i].unhighlightCallback.apply(this, arguments);
260             }
261             continue;
262           }
263           gs[i].clearSelection();
264         }
265         block = false;
266       }
267     }, true /* no need to redraw */);
268   }
269 }
270
271 Dygraph.synchronize = synchronize;
272
273 })();