1 /** 2 * @license 3 * Part of dygraphs, see top-level LICENSE.txt file 4 * MIT-licenced: https://opensource.org/licenses/MIT 5 */ 6 /** 7 * Synchronize zooming and/or selections between a set of dygraphs. 8 * 9 * Usage: 10 * 11 * var g1 = new Dygraph(...), 12 * g2 = new Dygraph(...), 13 * ...; 14 * var sync = Dygraph.synchronize(g1, g2, ...); 15 * // charts are now synchronized 16 * sync.detach(); 17 * // charts are no longer synchronized 18 * 19 * You can set options using the last parameter, for example: 20 * 21 * var sync = Dygraph.synchronize(g1, g2, g3, { 22 * selection: true, 23 * zoom: true 24 * }); 25 * 26 * The default is to synchronize both of these. 27 * 28 * Instead of passing one Dygraph object as each parameter, you may also pass an 29 * array of dygraphs: 30 * 31 * var sync = Dygraph.synchronize([g1, g2, g3], { 32 * selection: false, 33 * zoom: true 34 * }); 35 * 36 * You may also set `range: false` if you wish to only sync the x-axis. 37 * The `range` option has no effect unless `zoom` is true (the default). 38 */ 39 (function() { 40 /* global Dygraph:false */ 41 'use strict'; 42 43 var Dygraph; 44 if (window.Dygraph) { 45 Dygraph = window.Dygraph; 46 } else if (typeof(module) !== 'undefined') { 47 Dygraph = require('../dygraph'); 48 } 49 50 var synchronize = function(/* dygraphs..., opts */) { 51 if (arguments.length === 0) { 52 throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.'; 53 } 54 55 var OPTIONS = ['selection', 'zoom', 'range']; 56 var opts = { 57 selection: true, 58 zoom: true, 59 range: true 60 }; 61 var dygraphs = []; 62 var prevCallbacks = []; 63 64 var parseOpts = function(obj) { 65 if (!(obj instanceof Object)) { 66 throw 'Last argument must be either Dygraph or Object.'; 67 } else { 68 for (var i = 0; i < OPTIONS.length; i++) { 69 var optName = OPTIONS[i]; 70 if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName]; 71 } 72 } 73 }; 74 75 if (arguments[0] instanceof Dygraph) { 76 // Arguments are Dygraph objects. 77 for (var i = 0; i < arguments.length; i++) { 78 if (arguments[i] instanceof Dygraph) { 79 dygraphs.push(arguments[i]); 80 } else { 81 break; 82 } 83 } 84 if (i < arguments.length - 1) { 85 throw 'Invalid invocation of Dygraph.synchronize(). ' + 86 'All but the last argument must be Dygraph objects.'; 87 } else if (i == arguments.length - 1) { 88 parseOpts(arguments[arguments.length - 1]); 89 } 90 } else if (arguments[0].length) { 91 // Invoked w/ list of dygraphs, options 92 for (var i = 0; i < arguments[0].length; i++) { 93 dygraphs.push(arguments[0][i]); 94 } 95 if (arguments.length == 2) { 96 parseOpts(arguments[1]); 97 } else if (arguments.length > 2) { 98 throw 'Invalid invocation of Dygraph.synchronize(). ' + 99 'Expected two arguments: array and optional options argument.'; 100 } // otherwise arguments.length == 1, which is fine. 101 } else { 102 throw 'Invalid invocation of Dygraph.synchronize(). ' + 103 'First parameter must be either Dygraph or list of Dygraphs.'; 104 } 105 106 if (dygraphs.length < 2) { 107 throw 'Invalid invocation of Dygraph.synchronize(). ' + 108 'Need two or more dygraphs to synchronize.'; 109 } 110 111 var readycount = dygraphs.length; 112 for (var i = 0; i < dygraphs.length; i++) { 113 var g = dygraphs[i]; 114 g.ready( function() { 115 if (--readycount == 0) { 116 // store original callbacks 117 var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback']; 118 for (var j = 0; j < dygraphs.length; j++) { 119 if (!prevCallbacks[j]) { 120 prevCallbacks[j] = {}; 121 } 122 for (var k = callBackTypes.length - 1; k >= 0; k--) { 123 prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]); 124 } 125 } 126 127 // Listen for draw, highlight, unhighlight callbacks. 128 if (opts.zoom) { 129 attachZoomHandlers(dygraphs, opts, prevCallbacks); 130 } 131 132 if (opts.selection) { 133 attachSelectionHandlers(dygraphs, prevCallbacks); 134 } 135 } 136 }); 137 } 138 139 return { 140 detach: function() { 141 for (var i = 0; i < dygraphs.length; i++) { 142 var g = dygraphs[i]; 143 if (opts.zoom) { 144 g.updateOptions({drawCallback: prevCallbacks[i].drawCallback}); 145 } 146 if (opts.selection) { 147 g.updateOptions({ 148 highlightCallback: prevCallbacks[i].highlightCallback, 149 unhighlightCallback: prevCallbacks[i].unhighlightCallback 150 }); 151 } 152 } 153 // release references & make subsequent calls throw. 154 dygraphs = null; 155 opts = null; 156 prevCallbacks = null; 157 } 158 }; 159 }; 160 161 function arraysAreEqual(a, b) { 162 if (!Array.isArray(a) || !Array.isArray(b)) return false; 163 var i = a.length; 164 if (i !== b.length) return false; 165 while (i--) { 166 if (a[i] !== b[i]) return false; 167 } 168 return true; 169 } 170 171 function attachZoomHandlers(gs, syncOpts, prevCallbacks) { 172 var block = false; 173 for (var i = 0; i < gs.length; i++) { 174 var g = gs[i]; 175 g.updateOptions({ 176 drawCallback: function(me, initial) { 177 if (block || initial) return; 178 block = true; 179 var opts = { 180 dateWindow: me.xAxisRange() 181 }; 182 if (syncOpts.range) opts.valueRange = me.yAxisRange(); 183 184 for (var j = 0; j < gs.length; j++) { 185 if (gs[j] == me) { 186 if (prevCallbacks[j] && prevCallbacks[j].drawCallback) { 187 prevCallbacks[j].drawCallback.apply(this, arguments); 188 } 189 continue; 190 } 191 192 // Only redraw if there are new options 193 if (arraysAreEqual(opts.dateWindow, gs[j].getOption('dateWindow')) && 194 arraysAreEqual(opts.valueRange, gs[j].getOption('valueRange'))) { 195 continue; 196 } 197 198 gs[j].updateOptions(opts); 199 } 200 block = false; 201 } 202 }, true /* no need to redraw */); 203 } 204 } 205 206 function attachSelectionHandlers(gs, prevCallbacks) { 207 var block = false; 208 for (var i = 0; i < gs.length; i++) { 209 var g = gs[i]; 210 211 g.updateOptions({ 212 highlightCallback: function(event, x, points, row, seriesName) { 213 if (block) return; 214 block = true; 215 var me = this; 216 for (var i = 0; i < gs.length; i++) { 217 if (me == gs[i]) { 218 if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) { 219 prevCallbacks[i].highlightCallback.apply(this, arguments); 220 } 221 continue; 222 } 223 var idx = gs[i].getRowForX(x); 224 if (idx !== null) { 225 gs[i].setSelection(idx, seriesName, undefined, true); 226 } 227 } 228 block = false; 229 }, 230 unhighlightCallback: function(event) { 231 if (block) return; 232 block = true; 233 var me = this; 234 for (var i = 0; i < gs.length; i++) { 235 if (me == gs[i]) { 236 if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) { 237 prevCallbacks[i].unhighlightCallback.apply(this, arguments); 238 } 239 continue; 240 } 241 gs[i].clearSelection(); 242 } 243 block = false; 244 } 245 }, true /* no need to redraw */); 246 } 247 } 248 249 Dygraph.synchronize = synchronize; 250 251 })(); 252