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