X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=src%2Fextras%2Fsynchronizer.js;fp=src%2Fextras%2Fsynchronizer.js;h=16e4d96bc36926a8a0a502bc7d20c294c705a3a5;hb=3123ca57f71d145bb5bcc4a2f754d3dff3225346;hp=0000000000000000000000000000000000000000;hpb=26ee953643ccd2d32e38e6b60b20e6a01c1dc9ba;p=dygraphs.git diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js new file mode 100644 index 0000000..16e4d96 --- /dev/null +++ b/src/extras/synchronizer.js @@ -0,0 +1,206 @@ +/** + * Synchronize zooming and/or selections between a set of dygraphs. + * + * Usage: + * + * var g1 = new Dygraph(...), + * g2 = new Dygraph(...), + * ...; + * var sync = Dygraph.synchronize(g1, g2, ...); + * // charts are now synchronized + * sync.detach(); + * // charts are no longer synchronized + * + * You can set options using the last parameter, for example: + * + * var sync = Dygraph.synchronize(g1, g2, g3, { + * selection: true, + * zoom: true + * }); + * + * The default is to synchronize both of these. + * + * Instead of passing one Dygraph object as each parameter, you may also pass an + * array of dygraphs: + * + * var sync = Dygraph.synchronize([g1, g2, g3], { + * selection: false, + * zoom: true + * }); + * + * You may also set `range: false` if you wish to only sync the x-axis. + * The `range` option has no effect unless `zoom` is true (the default). + */ +(function() { +/* global Dygraph:false */ +'use strict'; + +Dygraph.synchronize = function(/* dygraphs..., opts */) { + if (arguments.length === 0) { + throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.'; + } + + var OPTIONS = ['selection', 'zoom', 'range']; + var opts = { + selection: true, + zoom: true, + range: true + }; + var dygraphs = []; + + var prevCallbacks = { + draw: null, + highlight: null, + unhighlight: null + }; + + var parseOpts = function(obj) { + if (!(obj instanceof Object)) { + throw 'Last argument must be either Dygraph or Object.'; + } else { + for (var i = 0; i < OPTIONS.length; i++) { + var optName = OPTIONS[i]; + if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName]; + } + } + }; + + if (arguments[0] instanceof Dygraph) { + // Arguments are Dygraph objects. + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Dygraph) { + dygraphs.push(arguments[i]); + } else { + break; + } + } + if (i < arguments.length - 1) { + throw 'Invalid invocation of Dygraph.synchronize(). ' + + 'All but the last argument must be Dygraph objects.'; + } else if (i == arguments.length - 1) { + parseOpts(arguments[arguments.length - 1]); + } + } else if (arguments[0].length) { + // Invoked w/ list of dygraphs, options + for (var i = 0; i < arguments[0].length; i++) { + dygraphs.push(arguments[0][i]); + } + if (arguments.length == 2) { + parseOpts(arguments[1]); + } else if (arguments.length > 2) { + throw 'Invalid invocation of Dygraph.synchronize(). ' + + 'Expected two arguments: array and optional options argument.'; + } // otherwise arguments.length == 1, which is fine. + } else { + throw 'Invalid invocation of Dygraph.synchronize(). ' + + 'First parameter must be either Dygraph or list of Dygraphs.'; + } + + if (dygraphs.length < 2) { + throw 'Invalid invocation of Dygraph.synchronize(). ' + + 'Need two or more dygraphs to synchronize.'; + } + + var readycount = dygraphs.length; + for (var i = 0; i < dygraphs.length; i++) { + var g = dygraphs[i]; + g.ready( function() { + if (--readycount == 0) { + // Listen for draw, highlight, unhighlight callbacks. + if (opts.zoom) { + attachZoomHandlers(dygraphs, opts, prevCallbacks); + } + + if (opts.selection) { + attachSelectionHandlers(dygraphs, prevCallbacks); + } + } + }); + } + + return { + detach: function() { + for (var i = 0; i < dygraphs.length; i++) { + var g = dygraphs[i]; + if (opts.zoom) { + g.updateOptions({drawCallback: prevCallbacks.draw}); + } + if (opts.selection) { + g.updateOptions({ + highlightCallback: prevCallbacks.highlight, + unhighlightCallback: prevCallbacks.unhighlight + }); + } + } + // release references & make subsequent calls throw. + dygraphs = null; + opts = null; + prevCallbacks = null; + } + }; +}; + +function attachZoomHandlers(gs, syncOpts, prevCallbacks) { + var block = false; + for (var i = 0; i < gs.length; i++) { + var g = gs[i]; + prevCallbacks.draw = g.getFunctionOption('drawCallback'); + g.updateOptions({ + drawCallback: function(me, initial) { + if (prevCallbacks.draw) prevCallbacks.draw(me, initial); + if (block || initial) return; + block = true; + var opts = { + dateWindow: me.xAxisRange() + }; + if (syncOpts.range) opts.valueRange = me.yAxisRange(); + + for (var j = 0; j < gs.length; j++) { + if (gs[j] == me) continue; + gs[j].updateOptions(opts); + } + block = false; + } + }, false /* no need to redraw */); + } +} + +function attachSelectionHandlers(gs, prevCallbacks) { + var block = false; + for (var i = 0; i < gs.length; i++) { + var g = gs[i]; + prevCallbacks.highlight = g.getFunctionOption('highlightCallback'); + prevCallbacks.unhighlight = g.getFunctionOption('unhighlightCallback'); + g.updateOptions({ + highlightCallback: function(event, x, points, row, seriesName) { + if (prevCallbacks.highlight) { + prevCallbacks.highlight(event, x, points, row, seriesName); + } + if (block) return; + block = true; + var me = this; + for (var i = 0; i < gs.length; i++) { + if (me == gs[i]) continue; + var idx = gs[i].getRowForX(x); + if (idx !== null) { + gs[i].setSelection(idx, seriesName); + } + } + block = false; + }, + unhighlightCallback: function(event) { + if (prevCallbacks.unhighlight) prevCallbacks.unhighlight(event); + if (block) return; + block = true; + var me = this; + for (var i = 0; i < gs.length; i++) { + if (me == gs[i]) continue; + gs[i].clearSelection(); + } + block = false; + } + }); + } +} + +})();