// Copyright (c) 2009 Dan Vanderkam. All rights reserved. // // SPDX-License-Identifier: MIT /** * 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). * * Original source: https://github.com/danvk/dygraphs/blob/master/src/extras/synchronizer.js * at commit b55a71d768d2f8de62877c32b3aec9e9975ac389 * * Copyright (c) 2009 Dan Vanderkam * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ (function() { /* global Dygraph:false */ 'use strict'; var Dygraph; if (window.Dygraph) { Dygraph = window.Dygraph; } else if (typeof(module) !== 'undefined') { Dygraph = require('../dygraph'); } var 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 = []; 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) { // store original callbacks var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback']; for (var j = 0; j < dygraphs.length; j++) { if (!prevCallbacks[j]) { prevCallbacks[j] = {}; } for (var k = callBackTypes.length - 1; k >= 0; k--) { prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]); } } // 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[i].drawCallback}); } if (opts.selection) { g.updateOptions({ highlightCallback: prevCallbacks[i].highlightCallback, unhighlightCallback: prevCallbacks[i].unhighlightCallback }); } } // release references & make subsequent calls throw. dygraphs = null; opts = null; prevCallbacks = null; } }; }; function arraysAreEqual(a, b) { if (!Array.isArray(a) || !Array.isArray(b)) return false; var i = a.length; if (i !== b.length) return false; while (i--) { if (a[i] !== b[i]) return false; } return true; } function attachZoomHandlers(gs, syncOpts, prevCallbacks) { var block = false; for (var i = 0; i < gs.length; i++) { var g = gs[i]; g.updateOptions({ drawCallback: function(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) { if (prevCallbacks[j] && prevCallbacks[j].drawCallback) { prevCallbacks[j].drawCallback.apply(this, arguments); } continue; } // Only redraw if there are new options if (arraysAreEqual(opts.dateWindow, gs[j].getOption('dateWindow')) && arraysAreEqual(opts.valueRange, gs[j].getOption('valueRange'))) { continue; } gs[j].updateOptions(opts); } block = false; } }, true /* no need to redraw */); } } function attachSelectionHandlers(gs, prevCallbacks) { var block = false; for (var i = 0; i < gs.length; i++) { var g = gs[i]; g.updateOptions({ highlightCallback: function(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]) { if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) { prevCallbacks[i].highlightCallback.apply(this, arguments); } continue; } var idx = gs[i].getRowForX(x); if (idx !== null) { gs[i].setSelection(idx, seriesName); } } block = false; }, unhighlightCallback: function(event) { if (block) return; block = true; var me = this; for (var i = 0; i < gs.length; i++) { if (me == gs[i]) { if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) { prevCallbacks[i].unhighlightCallback.apply(this, arguments); } continue; } gs[i].clearSelection(); } block = false; } }, true /* no need to redraw */); } } Dygraph.synchronize = synchronize; })();