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