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