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