--- /dev/null
+/**
+ * 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 objet as each parameter, you may also pass an
+ * array of dygraphs:
+ *
+ * var sync = Dygraph.synchronize([g1, g2, g3], {
+ * selection: false,
+ * zoom: true
+ * });
+ */
+(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'];
+ var opts = {
+ selection: true,
+ zoom: true
+ };
+ var dygraphs = [];
+
+ 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.';
+ }
+
+ // Listen for draw, highlight, unhighlight callbacks.
+ if (opts.zoom) {
+ attachZoomHandlers(dygraphs);
+ }
+
+ if (opts.selection) {
+ attachSelectionHandlers(dygraphs);
+ }
+
+ return {
+ detach: function() {
+ for (var i = 0; i < dygraphs.length; i++) {
+ var g = dygraphs[i];
+ if (opts.zoom) {
+ g.updateOptions({drawCallback: null});
+ }
+ if (opts.selection) {
+ g.updateOptions({
+ highlightCallback: null,
+ unhighlightCallback: null
+ });
+ }
+ }
+ // release references & make subsequent calls throw.
+ dygraphs = null;
+ opts = null;
+ }
+ };
+};
+
+// TODO: call any `drawCallback`s that were set before this.
+function attachZoomHandlers(gs) {
+ 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 range = me.xAxisRange();
+ var yrange = me.yAxisRange();
+ for (var j = 0; j < gs.length; j++) {
+ if (gs[j] == me) continue;
+ gs[j].updateOptions( {
+ dateWindow: range,
+ valueRange: yrange
+ });
+ }
+ block = false;
+ }
+ }, false /* no need to redraw */);
+ }
+}
+
+function attachSelectionHandlers(gs) {
+ 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]) continue;
+ var idx = dygraphsBinarySearch(gs[i], 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]) continue;
+ gs[i].clearSelection();
+ }
+ block = false;
+ }
+ });
+ }
+}
+
+// Returns the index corresponding to xVal, or null if there is none.
+function dygraphsBinarySearch(g, xVal) {
+ var low = 0,
+ high = g.numRows() - 1;
+
+ while (low < high) {
+ var idx = (high + low) >> 1;
+ var x = g.getValue(idx, 0);
+ if (x < xVal) {
+ low = idx + 1;
+ } else if (x > xVal) {
+ high = idx - 1;
+ } else {
+ return idx;
+ }
+ }
+
+ // TODO: give an option to find the closest point, i.e. not demand an exact match.
+ return null;
+}
+
+})();
<script type="text/javascript" src="dygraph-combined.js"></script>
-->
<script type="text/javascript" src="../dygraph-dev.js"></script>
+ <script type="text/javascript" src="../extras/synchronizer.js"></script>
<script type="text/javascript" src="data.js"></script>
<style type="text/css">
- #div1 { position: absolute; left: 10px; top: 30px; }
- #div2 { position: absolute; left: 520px; top: 30px; }
- #div3 { position: absolute; left: 10px; top: 340px; }
- #div4 { position: absolute; left: 520px; top: 340px; }
+ .chart { width: 500px; height: 300px; }
+ .chart-container { overflow: hidden; }
+ #div1 { float: left; }
+ #div2 { float: left; }
+ #div3 { float: left; clear: left; }
+ #div4 { float: left; }
</style>
</head>
<body>
<p>Zooming and panning on any of the charts will zoom and pan all the
- others.</p>
+ others. Selecting points on one will select points on the others.</p>
- <div id="div1" style="width:500px; height:300px;"></div>
- <div id="div2" style="width:500px; height:300px;"></div>
- <div id="div3" style="width:500px; height:300px;"></div>
- <div id="div4" style="width:500px; height:300px;"></div>
+ <p>To use this, source <a href="https://github.com/danvk/dygraphs/blob/master/extras/synchronizer.js"><code>extras/synchronizer.js</code></a> on your page.
+ See the comments in that file for usage.</p>
+
+ <div class="chart-container">
+ <div id="div1" class="chart"></div>
+ <div id="div2" class="chart"></div>
+ <div id="div3" class="chart"></div>
+ <div id="div4" class="chart"></div>
+ </div>
+
+ <p>
+ Synchronize what?
+ <input type=checkbox id='chk-zoom' checked onChange='update()'><label for='chk-zoom'> Zoom</label>
+ <input type=checkbox id='chk-selection' checked onChange='update()'><label for='chk-selection'> Selection</label>
+ </p>
<script type="text/javascript">
gs = [];
var blockRedraw = false;
- var initialized = false;
for (var i = 1; i <= 4; i++) {
gs.push(
new Dygraph(
NoisyData, {
rollPeriod: 7,
errorBars: true,
- drawCallback: function(me, initial) {
- if (blockRedraw || initial) return;
- blockRedraw = true;
- var range = me.xAxisRange();
- var yrange = me.yAxisRange();
- for (var j = 0; j < 4; j++) {
- if (gs[j] == me) continue;
- gs[j].updateOptions( {
- dateWindow: range,
- valueRange: yrange
- } );
- }
- blockRedraw = false;
- }
}
)
);
}
+ var sync = Dygraph.synchronize(gs);
+
+ function update() {
+ var zoom = document.getElementById('chk-zoom').checked;
+ var selection = document.getElementById('chk-selection').checked;
+ sync.detach();
+ sync = Dygraph.synchronize(gs, {
+ zoom: zoom,
+ selection: selection
+ });
+ }
</script>
</body>
</html>