cbe26d2622e3820fbc4dd72e03e951ae4c4a6784
[dygraphs.git] / extras / 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 (function() {
35 /* global Dygraph:false */
36 'use strict';
37
38 Dygraph.synchronize = function(/* dygraphs..., opts */) {
39 if (arguments.length === 0) {
40 throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
41 }
42
43 var OPTIONS = ['selection', 'zoom', 'range'];
44 var opts = {
45 selection: true,
46 zoom: true,
47 range: true
48 };
49 var dygraphs = [];
50
51 var prevCallbacks = {
52 draw: null,
53 highlight: null,
54 unhighlight: null
55 };
56
57 var parseOpts = function(obj) {
58 if (!(obj instanceof Object)) {
59 throw 'Last argument must be either Dygraph or Object.';
60 } else {
61 for (var i = 0; i < OPTIONS.length; i++) {
62 var optName = OPTIONS[i];
63 if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
64 }
65 }
66 };
67
68 if (arguments[0] instanceof Dygraph) {
69 // Arguments are Dygraph objects.
70 for (var i = 0; i < arguments.length; i++) {
71 if (arguments[i] instanceof Dygraph) {
72 dygraphs.push(arguments[i]);
73 } else {
74 break;
75 }
76 }
77 if (i < arguments.length - 1) {
78 throw 'Invalid invocation of Dygraph.synchronize(). ' +
79 'All but the last argument must be Dygraph objects.';
80 } else if (i == arguments.length - 1) {
81 parseOpts(arguments[arguments.length - 1]);
82 }
83 } else if (arguments[0].length) {
84 // Invoked w/ list of dygraphs, options
85 for (var i = 0; i < arguments[0].length; i++) {
86 dygraphs.push(arguments[0][i]);
87 }
88 if (arguments.length == 2) {
89 parseOpts(arguments[1]);
90 } else if (arguments.length > 2) {
91 throw 'Invalid invocation of Dygraph.synchronize(). ' +
92 'Expected two arguments: array and optional options argument.';
93 } // otherwise arguments.length == 1, which is fine.
94 } else {
95 throw 'Invalid invocation of Dygraph.synchronize(). ' +
96 'First parameter must be either Dygraph or list of Dygraphs.';
97 }
98
99 if (dygraphs.length < 2) {
100 throw 'Invalid invocation of Dygraph.synchronize(). ' +
101 'Need two or more dygraphs to synchronize.';
102 }
103
104 // Listen for draw, highlight, unhighlight callbacks.
105 if (opts.zoom) {
106 attachZoomHandlers(dygraphs, opts, prevCallbacks);
107 }
108
109 if (opts.selection) {
110 attachSelectionHandlers(dygraphs, prevCallbacks);
111 }
112
113 return {
114 detach: function() {
115 for (var i = 0; i < dygraphs.length; i++) {
116 var g = dygraphs[i];
117 if (opts.zoom) {
118 g.updateOptions({drawCallback: prevCallbacks.draw});
119 }
120 if (opts.selection) {
121 g.updateOptions({
122 highlightCallback: prevCallbacks.highlight,
123 unhighlightCallback: prevCallbacks.unhighlight
124 });
125 }
126 }
127 // release references & make subsequent calls throw.
128 dygraphs = null;
129 opts = null;
130 prevCallbacks = null;
131 }
132 };
133 };
134
135 function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
136 var block = false;
137 for (var i = 0; i < gs.length; i++) {
138 var g = gs[i];
139 prevCallbacks.draw = g.getFunctionOption('drawCallback');
140 g.updateOptions({
141 drawCallback: function(me, initial) {
142 if (prevCallbacks.draw) prevCallbacks.draw(me, initial);
143 if (block || initial) return;
144 block = true;
145 var opts = {
146 dateWindow: me.xAxisRange()
147 };
148 if (syncOpts.range) opts.valueRange = me.yAxisRange();
149
150 for (var j = 0; j < gs.length; j++) {
151 if (gs[j] == me) continue;
152 gs[j].updateOptions(opts);
153 }
154 block = false;
155 }
156 }, false /* no need to redraw */);
157 }
158 }
159
160 function attachSelectionHandlers(gs, prevCallbacks) {
161 var block = false;
162 for (var i = 0; i < gs.length; i++) {
163 var g = gs[i];
164 prevCallbacks.highlight = g.getFunctionOption('highlightCallback');
165 prevCallbacks.unhighlight = g.getFunctionOption('unhighlightCallback');
166 g.updateOptions({
167 highlightCallback: function(event, x, points, row, seriesName) {
168 if (prevCallbacks.highlight) {
169 prevCallbacks.highlight(event, x, points, row, seriesName);
170 }
171 if (block) return;
172 block = true;
173 var me = this;
174 for (var i = 0; i < gs.length; i++) {
175 if (me == gs[i]) continue;
176 var idx = dygraphsBinarySearch(gs[i], x);
177 if (idx !== null) {
178 gs[i].setSelection(idx, seriesName);
179 }
180 }
181 block = false;
182 },
183 unhighlightCallback: function(event) {
184 if (prevCallbacks.unhighlight) prevCallbacks.unhighlight(event);
185 if (block) return;
186 block = true;
187 var me = this;
188 for (var i = 0; i < gs.length; i++) {
189 if (me == gs[i]) continue;
190 gs[i].clearSelection();
191 }
192 block = false;
193 }
194 });
195 }
196 }
197
198 // Returns the index corresponding to xVal, or null if there is none.
199 function dygraphsBinarySearch(g, xVal) {
200 var low = 0,
201 high = g.numRows() - 1;
202
203 while (low <= high) {
204 var idx = (high + low) >> 1;
205 var x = g.getValue(idx, 0);
206 if (x < xVal) {
207 low = idx + 1;
208 } else if (x > xVal) {
209 high = idx - 1;
210 } else {
211 return idx;
212 }
213 }
214
215 // TODO: give an option to find the closest point, i.e. not demand an exact match.
216 return null;
217 }
218
219 })();