From c1a56a114c3d02ad72731b8aaebe5b15af33184c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 21 Dec 2022 13:05:33 +0100 Subject: [PATCH 01/11] Approximate constant replay speed Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 2099f300f..a96ce0ed4 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -243,6 +243,7 @@ storeStartDate = new Date('{{ event_starts_after }}'); storeEndDate = new Date('{{ event_ends_before }}'); {% endif %} + let replaySpeed = 100 async function embedAndLoad(chartSpecsPath, elementId, datasetName, previousResult, startDate, endDate) { @@ -471,7 +472,7 @@ replayBeliefsData(result[0]); }).catch(console.error); - const timer = ms => new Promise(res => setTimeout(res, ms)); + const timer = ms => new Promise(res => setTimeout(res, Math.max(ms, 0))); /** * Replays beliefs data. @@ -491,6 +492,7 @@ if (document.getElementById('replay').classList.contains('stopped') ) { break; } + var s = performance.now(); beliefTime = new Date(beliefTime.getTime() + beliefTimedelta); // Split off one replay step of new data from the remaining data @@ -516,7 +518,11 @@ vegaView.run().finalize(); document.getElementById('replay-time').innerHTML = beliefTime; - await timer(25); + + // Approximate constant speed + var e = performance.now(); + var throttle = e - s; + await timer(replaySpeed - throttle); } // Remove replay ruler vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize(); From d298f5376ccd7f7339da2050b184c9957f3ee706 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 21 Dec 2022 13:18:12 +0100 Subject: [PATCH 02/11] Listen to p-key presses to pause/resume Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index a96ce0ed4..d4ecbb881 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -538,6 +538,21 @@ toggle.classList.add('playing'); } }); + + window.addEventListener('keypress', function(e) { + // Pause/resume replay with 'p' + if (e.key==='p') { + if (toggle.classList.contains('playing')) { + // Pause replay + toggle.classList.remove('playing'); + toggle.classList.add('paused'); + } else if (toggle.classList.contains('paused')) { + // Resume replay + toggle.classList.remove('paused'); + toggle.classList.add('playing'); + } + } + }, false); {% endblock sensorChartSetup %} From 321dc5d02d9eff791fffa02c9bd66d886deb7d58 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 21 Dec 2022 13:31:16 +0100 Subject: [PATCH 03/11] Refactor: also start replay when hitting 'p' Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 211 +++++++++++++++------------- 1 file changed, 116 insertions(+), 95 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index d4ecbb881..12a6ca663 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -447,112 +447,133 @@ toggle.addEventListener('click', function(e) { e.preventDefault(); if (toggle.classList.contains('playing')) { - // Pause replay - toggle.classList.remove('playing'); - toggle.classList.add('paused'); + pauseReplay(); } else if (toggle.classList.contains('stopped')) { - // Start replay - toggle.classList.remove('stopped'); - toggle.classList.add('playing'); - var beliefTime = new Date(storeStartDate); - var numReplaySteps = Math.ceil((storeEndDate - storeStartDate) / beliefTimedelta); - queryStartDate = (storeStartDate != null) ? (storeStartDate.toISOString()) : (null); - queryEndDate = (storeEndDate != null) ? (storeEndDate.toISOString()) : (null); - - $("#spinner").show(); - Promise.all([ - // Fetch time series data (all data, not only the most recent beliefs) - fetch(dataPath + '/chart_data/?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate + '&most_recent_beliefs_only=false', { - method: "GET", - headers: {"Content-Type": "application/json"}, - }) - .then(function(response) { return response.json(); }), - ]).then(function(result) { - $("#spinner").hide(); - replayBeliefsData(result[0]); - }).catch(console.error); - - const timer = ms => new Promise(res => setTimeout(res, Math.max(ms, 0))); - - /** - * Replays beliefs data. - * - * As we go forward in time in steps, replayedData is updated with newData that was known at beliefTime, - * by splitting off newData from remainingData. - * Then, replayedData is loaded into the chart. - * - * @param {Array} remainingData Array containing beliefs. - */ - async function replayBeliefsData (remainingData) { - var replayedData = []; - for (var i = 0; i < numReplaySteps + 1; i++) { - while (document.getElementById('replay').classList.contains('paused') ) { - await timer(1000); - } - if (document.getElementById('replay').classList.contains('stopped') ) { - break; - } - var s = performance.now(); - beliefTime = new Date(beliefTime.getTime() + beliefTimedelta); - - // Split off one replay step of new data from the remaining data - var newData; - [newData, remainingData] = partition( - remainingData, - (item) => item.belief_time <= beliefTime.getTime(), - ); - - // Update beliefs in the replayed data given the new data - replayedData = updateBeliefs(replayedData, newData); - - /** When selecting a longer time periode (more than a week), the replay slows down a bit. This - * seems to be mainly from reloading the data into the graph. Slicing the data takes 10-30 ms, and - * loading that data into the graph takes 30-200 ms, depending on how much data is shown in the - * graph. After trying different approaches, we fell back to the original approach of telling vega - * to remove all previous data and to insert a completely new dataset at each iteration. Updating - * the view with removing only a few data points (representing obsolete beliefs) and inserting only - * a few data points (representing the most recent new beliefs) actually made it slower. - */ - vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(replayedData)); - vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': beliefTime})); - vegaView.run().finalize(); - - document.getElementById('replay-time').innerHTML = beliefTime; - - // Approximate constant speed - var e = performance.now(); - var throttle = e - s; - await timer(replaySpeed - throttle); - } - // Remove replay ruler - vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize(); - - // Stop replay when finished - toggle.classList.remove('playing'); - toggle.classList.add('stopped'); - document.getElementById('replay-time').innerHTML = ''; - } + startReplay(); } else { - // Resume replay - toggle.classList.remove('paused'); - toggle.classList.add('playing'); + resumeReplay(); } }); window.addEventListener('keypress', function(e) { // Pause/resume replay with 'p' if (e.key==='p') { - if (toggle.classList.contains('playing')) { - // Pause replay - toggle.classList.remove('playing'); - toggle.classList.add('paused'); - } else if (toggle.classList.contains('paused')) { - // Resume replay - toggle.classList.remove('paused'); - toggle.classList.add('playing'); + if (toggle.classList.contains('stopped')) { + startReplay(); + } else if (toggle.classList.contains('playing')) { + pauseReplay(); + } else { + resumeReplay(); } } }, false); + + /** + * Pause replay + */ + function pauseReplay() { + toggle.classList.remove('playing'); + toggle.classList.add('paused'); + } + /** + * Resume replay + */ + function resumeReplay() { + toggle.classList.remove('paused'); + toggle.classList.add('playing'); + } + /** + * Stop replay + */ + function stopReplay() { + toggle.classList.remove('playing'); + toggle.classList.add('stopped'); + + // Remove replay ruler and replay time + vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize(); + document.getElementById('replay-time').innerHTML = ''; + } + /** + * Start replay + */ + function startReplay() { + toggle.classList.remove('stopped'); + toggle.classList.add('playing'); + var beliefTime = new Date(storeStartDate); + var numReplaySteps = Math.ceil((storeEndDate - storeStartDate) / beliefTimedelta); + queryStartDate = (storeStartDate != null) ? (storeStartDate.toISOString()) : (null); + queryEndDate = (storeEndDate != null) ? (storeEndDate.toISOString()) : (null); + + $("#spinner").show(); + Promise.all([ + // Fetch time series data (all data, not only the most recent beliefs) + fetch(dataPath + '/chart_data/?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate + '&most_recent_beliefs_only=false', { + method: "GET", + headers: {"Content-Type": "application/json"}, + }) + .then(function(response) { return response.json(); }), + ]).then(function(result) { + $("#spinner").hide(); + replayBeliefsData(result[0]); + }).catch(console.error); + + const timer = ms => new Promise(res => setTimeout(res, Math.max(ms, 0))); + + /** + * Replays beliefs data. + * + * As we go forward in time in steps, replayedData is updated with newData that was known at beliefTime, + * by splitting off newData from remainingData. + * Then, replayedData is loaded into the chart. + * + * @param {Array} remainingData Array containing beliefs. + */ + async function replayBeliefsData (remainingData) { + var replayedData = []; + for (var i = 0; i < numReplaySteps + 1; i++) { + while (document.getElementById('replay').classList.contains('paused') ) { + await timer(1000); + } + if (document.getElementById('replay').classList.contains('stopped') ) { + break; + } + var s = performance.now(); + beliefTime = new Date(beliefTime.getTime() + beliefTimedelta); + + // Split off one replay step of new data from the remaining data + var newData; + [newData, remainingData] = partition( + remainingData, + (item) => item.belief_time <= beliefTime.getTime(), + ); + + // Update beliefs in the replayed data given the new data + replayedData = updateBeliefs(replayedData, newData); + + /** When selecting a longer time periode (more than a week), the replay slows down a bit. This + * seems to be mainly from reloading the data into the graph. Slicing the data takes 10-30 ms, and + * loading that data into the graph takes 30-200 ms, depending on how much data is shown in the + * graph. After trying different approaches, we fell back to the original approach of telling vega + * to remove all previous data and to insert a completely new dataset at each iteration. Updating + * the view with removing only a few data points (representing obsolete beliefs) and inserting only + * a few data points (representing the most recent new beliefs) actually made it slower. + */ + vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(replayedData)); + vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': beliefTime})); + vegaView.run().finalize(); + + document.getElementById('replay-time').innerHTML = beliefTime; + + // Approximate constant speed + var e = performance.now(); + var throttle = e - s; + await timer(replaySpeed - throttle); + } + + // Stop replay when finished + stopReplay() + } + } {% endblock sensorChartSetup %} From 40a1db96c662123a08effd0df2921b72261a2c37 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 21 Dec 2022 13:36:34 +0100 Subject: [PATCH 04/11] Refactor: clean up util functions Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 41 +++++++++-------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 12a6ca663..c3a6b8b04 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -446,45 +446,33 @@ let toggle = document.querySelector('#replay'); toggle.addEventListener('click', function(e) { e.preventDefault(); - if (toggle.classList.contains('playing')) { - pauseReplay(); - } else if (toggle.classList.contains('stopped')) { - startReplay(); - } else { - resumeReplay(); - } + toggleReplay(); }); window.addEventListener('keypress', function(e) { - // Pause/resume replay with 'p' + // Start/pause/resume replay with 'p' if (e.key==='p') { - if (toggle.classList.contains('stopped')) { - startReplay(); - } else if (toggle.classList.contains('playing')) { - pauseReplay(); - } else { - resumeReplay(); - } - } + toggleReplay(); + } }, false); - /** - * Pause replay - */ + function toggleReplay() { + if (toggle.classList.contains('stopped')) { + startReplay(); + } else if (toggle.classList.contains('playing')) { + pauseReplay(); + } else { + resumeReplay(); + } + } function pauseReplay() { toggle.classList.remove('playing'); toggle.classList.add('paused'); } - /** - * Resume replay - */ function resumeReplay() { toggle.classList.remove('paused'); toggle.classList.add('playing'); } - /** - * Stop replay - */ function stopReplay() { toggle.classList.remove('playing'); toggle.classList.add('stopped'); @@ -493,9 +481,6 @@ vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize(); document.getElementById('replay-time').innerHTML = ''; } - /** - * Start replay - */ function startReplay() { toggle.classList.remove('stopped'); toggle.classList.add('playing'); From e0c95c52e003db305b6cde762d5aab4f28bc3de7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 21 Dec 2022 13:39:31 +0100 Subject: [PATCH 05/11] Use stopReplay on calendar selection, too Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index c3a6b8b04..10c32e98c 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -326,12 +326,7 @@ format: 'YYYY-MM-DD\\T00:00:00', }); picker.on('selected', (startDate, endDate) => { - // Stop replay - let toggle = document.querySelector('#replay'); - toggle.classList.remove('playing'); - toggle.classList.remove('paused'); - toggle.classList.add('stopped'); - + stopReplay() startDate = startDate.toJSDate(); endDate = endDate.toJSDate(); endDate.setDate(endDate.getDate() + 1); @@ -475,6 +470,7 @@ } function stopReplay() { toggle.classList.remove('playing'); + toggle.classList.remove('paused'); toggle.classList.add('stopped'); // Remove replay ruler and replay time From 7d780963b5ef5ec3416d6284d3b0ced8aaf40c4d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 22 Dec 2022 10:10:41 +0100 Subject: [PATCH 06/11] Refactor: simplify for-loop Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 10c32e98c..8f5a2ffda 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -511,7 +511,7 @@ */ async function replayBeliefsData (remainingData) { var replayedData = []; - for (var i = 0; i < numReplaySteps + 1; i++) { + for (var beliefTime = new Date(storeStartDate); beliefTime <= storeEndDate; beliefTime = new Date(beliefTime.getTime() + beliefTimedelta)) { while (document.getElementById('replay').classList.contains('paused') ) { await timer(1000); } @@ -519,7 +519,6 @@ break; } var s = performance.now(); - beliefTime = new Date(beliefTime.getTime() + beliefTimedelta); // Split off one replay step of new data from the remaining data var newData; From 99fab638591982c5c52b2805c1f4122eef0763c4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 22 Dec 2022 13:33:49 +0100 Subject: [PATCH 07/11] Listen to s-key presses to stop Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 8f5a2ffda..24b68bbdb 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -448,6 +448,8 @@ // Start/pause/resume replay with 'p' if (e.key==='p') { toggleReplay(); + } else if (e.key==='s') { + stopReplay(); } }, false); From 22e93a27dce3de0a50670f0c70322957a5a2a09e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 Dec 2022 11:54:06 +0100 Subject: [PATCH 08/11] Show previous results when stopping replay Signed-off-by: F.N. Claessen --- flexmeasures/ui/templates/base.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 24b68bbdb..8f64e9fc2 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -326,7 +326,6 @@ format: 'YYYY-MM-DD\\T00:00:00', }); picker.on('selected', (startDate, endDate) => { - stopReplay() startDate = startDate.toJSDate(); endDate = endDate.toJSDate(); endDate.setDate(endDate.getDate() + 1); @@ -335,6 +334,8 @@ var queryStartDate = (startDate != null) ? (startDate.toISOString()) : (null); var queryEndDate = (endDate != null) ? (endDate.toISOString()) : (null); + stopReplay() + // Abort previous request and create abort controller for new request controller.abort(); controller = new AbortController(); @@ -470,7 +471,10 @@ toggle.classList.remove('paused'); toggle.classList.add('playing'); } - function stopReplay() { + async function stopReplay() { + if (toggle.classList.contains('stopped')) { + return; + } toggle.classList.remove('playing'); toggle.classList.remove('paused'); toggle.classList.add('stopped'); @@ -478,6 +482,11 @@ // Remove replay ruler and replay time vegaView.change('replay', vega.changeset().remove(vega.truthy).insert({'belief_time': null})).run().finalize(); document.getElementById('replay-time').innerHTML = ''; + + // Show previous results + $("#spinner").show(); + await embedAndLoad(chartSpecsPath + 'event_starts_after=' + storeStartDate.toISOString() + '&event_ends_before=' + storeEndDate.toISOString() + '&', elementId, datasetName, previousResult, storeStartDate, storeEndDate); + $("#spinner").hide(); } function startReplay() { toggle.classList.remove('stopped'); From d25014ad38001241de62c1162a2660746d74490c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 Dec 2022 13:16:34 +0100 Subject: [PATCH 09/11] Use abort signals to resolve glitches when pressing the next key before resolving promises Signed-off-by: F.N. Claessen --- flexmeasures/ui/static/js/replay-utils.js | 19 +++++++++++++++++++ flexmeasures/ui/templates/base.html | 17 +++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/flexmeasures/ui/static/js/replay-utils.js b/flexmeasures/ui/static/js/replay-utils.js index 34b3f266f..2ab52e3c9 100644 --- a/flexmeasures/ui/static/js/replay-utils.js +++ b/flexmeasures/ui/static/js/replay-utils.js @@ -42,3 +42,22 @@ export function updateBeliefs(oldBeliefs, newBeliefs) { // Define the step duration for the replay (value in ms) export var beliefTimedelta = 3600000 + + +/** + * Timer that can be canceled using the optional AbortSignal. + * Adapted from https://www.bennadel.com/blog/4195-using-abortcontroller-to-debounce-settimeout-calls-in-javascript.htm + * MIT License: https://www.bennadel.com/blog/license.htm + */ +export function setAbortableTimeout(callback, delayInMilliseconds, signal) { + signal?.addEventListener( "abort", handleAbort ); + var internalTimer = setTimeout(internalCallback, delayInMilliseconds); + + function internalCallback() { + signal?.removeEventListener( "abort", handleAbort ); + callback(); + } + function handleAbort() { + clearTimeout( internalTimer ); + } +} diff --git a/flexmeasures/ui/templates/base.html b/flexmeasures/ui/templates/base.html index 8f64e9fc2..6807f2647 100644 --- a/flexmeasures/ui/templates/base.html +++ b/flexmeasures/ui/templates/base.html @@ -231,7 +231,7 @@