From a8410795c9d215df3d5be71ca8489ff22b122a6e Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Sun, 9 Apr 2023 10:44:02 +0600 Subject: [PATCH 1/7] synchronizer add selectClosest option --- src/extras/synchronizer.js | 51 +++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index 0abd353a..9012db5c 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -56,9 +56,10 @@ var synchronize = function synchronize(/* dygraphs..., opts */) { throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.'; } - var OPTIONS = ['selection', 'zoom', 'range']; + var OPTIONS = ['selection', 'selectionClosest', 'zoom', 'range']; var opts = { selection: true, + selectionClosest: false, zoom: true, range: true }; @@ -134,7 +135,7 @@ var synchronize = function synchronize(/* dygraphs..., opts */) { } if (opts.selection) { - attachSelectionHandlers(dygraphs, prevCallbacks); + attachSelectionHandlers(dygraphs, opts, prevCallbacks); } } }); @@ -172,6 +173,41 @@ function arraysAreEqual(a, b) { return true; } +function closestIdx(gs, x) { + // if graph has no data or single entry + var highestI = gs.numRows() - 1 + if (highestI < 0) return null + if (highestI === 0) return 0 + + var lowestI = 0 + + // if values of x axis are in descending order, reverse searching borders + if (gs.getValue(0, 0) > gs.getValue(highestI, 0)) { + lowestI = highestI + highestI = 0 + } + + while (true) { + var middleI = Math.round( (lowestI + highestI) * 0.5 ) + if (middleI === lowestI || middleI === highestI) break + + var middleX = gs.getValue(middleI, 0) + if (middleX === x) return middleI + + if (x < middleX) { + highestI = middleI + } else { + lowestI = middleI + } + } + + var lowestValue = gs.getValue(lowestI, 0) + var highestValue = gs.getValue(highestI, 0) + var closestI = x - lowestValue < highestValue - x ? lowestI : highestI + + return closestI +} + function attachZoomHandlers(gs, syncOpts, prevCallbacks) { var block = false; for (var i = 0; i < gs.length; i++) { @@ -223,7 +259,7 @@ function attachZoomHandlers(gs, syncOpts, prevCallbacks) { } } -function attachSelectionHandlers(gs, prevCallbacks) { +function attachSelectionHandlers(gs, syncOpts, prevCallbacks) { var block = false; for (var i = 0; i < gs.length; i++) { var g = gs[i]; @@ -240,7 +276,14 @@ function attachSelectionHandlers(gs, prevCallbacks) { } continue; } - var idx = gs[i].getRowForX(x); + var idx + if (!syncOpts.selectionClosest) { + idx = gs[i].getRowForX(x); + } else { + idx = null + if (gs[i].numRows() === me.numRows()) idx = gs[i].getRowForX(x); + if (idx === null) idx = closestIdx(gs[i], x) + } if (idx !== null) { gs[i].setSelection(idx, seriesName, undefined, true); } From 96903b6baeff0c930af72ee84c20cc857cd5c5ac Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Tue, 16 May 2023 13:59:05 +0600 Subject: [PATCH 2/7] add ; where it was forgotten for coding style consistency --- src/extras/synchronizer.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index 9012db5c..841db242 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -175,37 +175,37 @@ function arraysAreEqual(a, b) { function closestIdx(gs, x) { // if graph has no data or single entry - var highestI = gs.numRows() - 1 - if (highestI < 0) return null - if (highestI === 0) return 0 + var highestI = gs.numRows() - 1; + if (highestI < 0) return null; + if (highestI === 0) return 0; - var lowestI = 0 + var lowestI = 0; // if values of x axis are in descending order, reverse searching borders if (gs.getValue(0, 0) > gs.getValue(highestI, 0)) { - lowestI = highestI - highestI = 0 + lowestI = highestI; + highestI = 0; } while (true) { - var middleI = Math.round( (lowestI + highestI) * 0.5 ) - if (middleI === lowestI || middleI === highestI) break + var middleI = Math.round( (lowestI + highestI) * 0.5 ); + if (middleI === lowestI || middleI === highestI) break; - var middleX = gs.getValue(middleI, 0) - if (middleX === x) return middleI + var middleX = gs.getValue(middleI, 0); + if (middleX === x) return middleI; if (x < middleX) { - highestI = middleI + highestI = middleI; } else { - lowestI = middleI + lowestI = middleI; } } - var lowestValue = gs.getValue(lowestI, 0) - var highestValue = gs.getValue(highestI, 0) - var closestI = x - lowestValue < highestValue - x ? lowestI : highestI + var lowestValue = gs.getValue(lowestI, 0); + var highestValue = gs.getValue(highestI, 0); + var closestI = x - lowestValue < highestValue - x ? lowestI : highestI; - return closestI + return closestI; } function attachZoomHandlers(gs, syncOpts, prevCallbacks) { @@ -276,13 +276,13 @@ function attachSelectionHandlers(gs, syncOpts, prevCallbacks) { } continue; } - var idx + var idx; if (!syncOpts.selectionClosest) { idx = gs[i].getRowForX(x); } else { - idx = null + idx = null; if (gs[i].numRows() === me.numRows()) idx = gs[i].getRowForX(x); - if (idx === null) idx = closestIdx(gs[i], x) + if (idx === null) idx = closestIdx(gs[i], x); } if (idx !== null) { gs[i].setSelection(idx, seriesName, undefined, true); From 6547227c0a73ab13da6418b7541f8140c00f8324 Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Tue, 16 May 2023 14:04:26 +0600 Subject: [PATCH 3/7] use layout_.points array for search --- src/extras/synchronizer.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index 841db242..12c3eb35 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -174,15 +174,17 @@ function arraysAreEqual(a, b) { } function closestIdx(gs, x) { + var points = gs.layout_.points[0]; + // if graph has no data or single entry - var highestI = gs.numRows() - 1; + var highestI = points.length - 1; if (highestI < 0) return null; - if (highestI === 0) return 0; + if (highestI === 0) return points[0].idx; var lowestI = 0; // if values of x axis are in descending order, reverse searching borders - if (gs.getValue(0, 0) > gs.getValue(highestI, 0)) { + if (points[0].xval > points[highestI].xval) { lowestI = highestI; highestI = 0; } @@ -191,8 +193,8 @@ function closestIdx(gs, x) { var middleI = Math.round( (lowestI + highestI) * 0.5 ); if (middleI === lowestI || middleI === highestI) break; - var middleX = gs.getValue(middleI, 0); - if (middleX === x) return middleI; + var middleX = points[middleI].xval; + if (middleX === x) return points[middleI].idx; if (x < middleX) { highestI = middleI; @@ -201,11 +203,9 @@ function closestIdx(gs, x) { } } - var lowestValue = gs.getValue(lowestI, 0); - var highestValue = gs.getValue(highestI, 0); - var closestI = x - lowestValue < highestValue - x ? lowestI : highestI; + var closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; - return closestI; + return points[closestI].idx; } function attachZoomHandlers(gs, syncOpts, prevCallbacks) { From cd528fceef7e4699065cb18b2cbfbb6e2bef8563 Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Tue, 16 May 2023 14:08:53 +0600 Subject: [PATCH 4/7] filter out selection when closest point is out of graph bounds --- src/extras/synchronizer.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index 12c3eb35..cb0b6f16 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -208,6 +208,21 @@ function closestIdx(gs, x) { return points[closestI].idx; } +function isInsideDateWindow(gs, idx) { + if (idx === null) return false; + var xAxisRange = gs.xAxisRange(); + var min, max; + if (xAxisRange[0] <= xAxisRange[1]) { + min = xAxisRange[0]; + max = xAxisRange[1]; + } else { + min = xAxisRange[1]; + max = xAxisRange[0]; + } + var xval = gs.getValue(idx, 0); + return xval >= min && xval <= max; +} + function attachZoomHandlers(gs, syncOpts, prevCallbacks) { var block = false; for (var i = 0; i < gs.length; i++) { @@ -284,8 +299,10 @@ function attachSelectionHandlers(gs, syncOpts, prevCallbacks) { if (gs[i].numRows() === me.numRows()) idx = gs[i].getRowForX(x); if (idx === null) idx = closestIdx(gs[i], x); } - if (idx !== null) { + if (isInsideDateWindow(gs[i], idx)) { gs[i].setSelection(idx, seriesName, undefined, true); + } else { + gs[i].clearSelection(); } } block = false; From 181989a59b97baf82c9406ceb159836cd939d2c0 Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Tue, 4 Jul 2023 05:19:52 +0600 Subject: [PATCH 5/7] select left point for graphs in stepPlot mode --- src/extras/synchronizer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index cb0b6f16..f9304928 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -203,7 +203,11 @@ function closestIdx(gs, x) { } } - var closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; + var closestI; + + // if graph in stepPlot mode, return left point, otherwise closest x value point + if (gs.getOption('stepPlot') === true) closestI = lowestI < highestI ? lowestI : highestI; + else closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; return points[closestI].idx; } From 1a78ffa0fe644e56e0b703e1a13bd70e37e18a46 Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:02:14 +0600 Subject: [PATCH 6/7] stepPlot - return right point if it matches the search --- src/extras/synchronizer.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index f9304928..1cf87cde 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -205,9 +205,18 @@ function closestIdx(gs, x) { var closestI; - // if graph in stepPlot mode, return left point, otherwise closest x value point - if (gs.getOption('stepPlot') === true) closestI = lowestI < highestI ? lowestI : highestI; - else closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; + // if graph in stepPlot mode, check right point for match + // if right point matched, return it, otherwise return left point + // if graph is not in stepPlot mode, return closest by x value point + if (gs.getOption('stepPlot') === true) { + if (lowestI < highestI) { + closestI = points[highestI].xval === x ? highestI : lowestI; + } else { + closestI = points[lowestI].xval === x ? lowestI : highestI; + } + } else { + closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; + } return points[closestI].idx; } From 749bcef24690b29f11d4f86888c3c7b38d552d16 Mon Sep 17 00:00:00 2001 From: n-gist <58081918+n-gist@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:04:34 +0500 Subject: [PATCH 7/7] add note for new option, coding style fixes, --- src/extras/synchronizer.js | 63 ++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/extras/synchronizer.js b/src/extras/synchronizer.js index 1cf87cde..570d5038 100644 --- a/src/extras/synchronizer.js +++ b/src/extras/synchronizer.js @@ -36,6 +36,11 @@ * * 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). + * + * Synchronizer selection option selects points by exact values on x-axes. + * If graphs have different x-axes values, you may use + * `selectionClosest: true` to select points closest to hovered ones, + * corresponding to their x values. */ /* loader wrapper to allow browser use and ES6 imports */ @@ -176,25 +181,29 @@ function arraysAreEqual(a, b) { function closestIdx(gs, x) { var points = gs.layout_.points[0]; - // if graph has no data or single entry - var highestI = points.length - 1; - if (highestI < 0) return null; - if (highestI === 0) return points[0].idx; + // If graph has no data or single entry + if (points.length === 0) + return null; + if (points.length === 1) + return points[0].idx; var lowestI = 0; + var highestI = points.length - 1; - // if values of x axis are in descending order, reverse searching borders + // If values of x axis are in descending order, reverse searching borders if (points[0].xval > points[highestI].xval) { lowestI = highestI; highestI = 0; } while (true) { - var middleI = Math.round( (lowestI + highestI) * 0.5 ); - if (middleI === lowestI || middleI === highestI) break; + var middleI = Math.round((lowestI + highestI) * 0.5); + if (middleI === lowestI || middleI === highestI) + break; var middleX = points[middleI].xval; - if (middleX === x) return points[middleI].idx; + if (middleX === x) + return points[middleI].idx; if (x < middleX) { highestI = middleI; @@ -205,24 +214,37 @@ function closestIdx(gs, x) { var closestI; - // if graph in stepPlot mode, check right point for match - // if right point matched, return it, otherwise return left point - // if graph is not in stepPlot mode, return closest by x value point + /* + * If graph in stepPlot mode, check right point for match + * If right point matched, return it, otherwise return left point + * If graph is not in stepPlot mode, return closest by x value point + */ if (gs.getOption('stepPlot') === true) { if (lowestI < highestI) { - closestI = points[highestI].xval === x ? highestI : lowestI; + if (points[highestI].xval === x) + closestI = highestI; + else + closestI = lowestI; } else { - closestI = points[lowestI].xval === x ? lowestI : highestI; + if (points[lowestI].xval === x) + closestI = lowestI; + else + closestI = highestI; } } else { - closestI = x - points[lowestI].xval < points[highestI].xval - x ? lowestI : highestI; + if (x - points[lowestI].xval <= points[highestI].xval - x) + closestI = lowestI; + else + closestI = highestI; } return points[closestI].idx; } function isInsideDateWindow(gs, idx) { - if (idx === null) return false; + if (idx === null) + return false; + var xAxisRange = gs.xAxisRange(); var min, max; if (xAxisRange[0] <= xAxisRange[1]) { @@ -309,14 +331,15 @@ function attachSelectionHandlers(gs, syncOpts, prevCallbacks) { idx = gs[i].getRowForX(x); } else { idx = null; - if (gs[i].numRows() === me.numRows()) idx = gs[i].getRowForX(x); - if (idx === null) idx = closestIdx(gs[i], x); + if (gs[i].numRows() === me.numRows()) + idx = gs[i].getRowForX(x); + if (idx === null) + idx = closestIdx(gs[i], x); } - if (isInsideDateWindow(gs[i], idx)) { + if (isInsideDateWindow(gs[i], idx)) gs[i].setSelection(idx, seriesName, undefined, true); - } else { + else gs[i].clearSelection(); - } } block = false; },