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