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