Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add replay button to sensor and asset pages #463

Merged
merged 39 commits into from Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
efd77e1
Add replay button to sensor page
Flix6x Jul 15, 2022
11f241d
Add replay time below replay button
Flix6x Jul 16, 2022
cd8ce8e
Include older beliefs in replay
Flix6x Jul 18, 2022
33b67a2
Show only most recent beliefs in replay at any given time, and add ne…
Flix6x Jul 21, 2022
7d346a9
Speed up: no need to resize
Flix6x Jul 21, 2022
15cab01
Make sure vega view is embedded in page before attempting to change i…
Flix6x Aug 8, 2022
3ed6847
Typo
Flix6x Aug 11, 2022
4697771
Add replay button to asset page
Flix6x Aug 11, 2022
e9cbcf4
Pass along sensor information as an object
Flix6x Aug 11, 2022
0cd4c2a
black
Flix6x Aug 11, 2022
0333e21
missing type annotations
Flix6x Aug 11, 2022
366cd4f
Nicer js formatting
Flix6x Aug 11, 2022
ce6d5b1
Remove obsolete code
Flix6x Aug 11, 2022
806b294
Refactor: move partition function into replay-utils.js
Flix6x Aug 11, 2022
92cf69e
Add dict representation of sensor to JSON representation of BeliefsDa…
Flix6x Aug 11, 2022
96e889a
Merge branch 'main' into replay
Flix6x Aug 11, 2022
67c0403
Merge branch 'main' into replay
Flix6x Aug 11, 2022
d6be297
Remove duplicate swipe functionality on the sensor page (already cont…
Flix6x Aug 11, 2022
715964a
Refactor: move sensor chart setup to base
Flix6x Aug 12, 2022
36e6846
Refactor: simplify
Flix6x Aug 12, 2022
96cdcf6
Refactor: rename and simplify
Flix6x Aug 12, 2022
04c401a
Add inline comments
Flix6x Sep 5, 2022
3eea7bb
Add docstrings to replay-utils.js
Flix6x Sep 5, 2022
2e2bf00
Rename parameter
Flix6x Sep 5, 2022
06891f2
Remove obsolete variable
Flix6x Sep 5, 2022
4b5a540
Rename simulation/playback to replay
Flix6x Sep 5, 2022
8e7703f
Rename function
Flix6x Sep 5, 2022
26df36b
Pass beliefs data rather than fetch result to replay function
Flix6x Sep 5, 2022
0c8bbe7
Add docstring to replay function
Flix6x Sep 5, 2022
a7e411b
Move beliefTimedelta to replay-utils.js
Flix6x Sep 5, 2022
8a0b88c
Remove logging statements
Flix6x Sep 5, 2022
d626855
Merge remote-tracking branch 'origin/main' into replay
Flix6x Sep 5, 2022
299cf37
Changelog entry
Flix6x Sep 5, 2022
abfa2c2
Merge remote-tracking branch 'origin/main' into replay
Flix6x Sep 5, 2022
75b286c
Merge remote-tracking branch 'origin/main' into replay
Flix6x Sep 6, 2022
89536e6
Add missing semicolons
Flix6x Sep 8, 2022
120de6d
Add inline comments
Flix6x Sep 8, 2022
266d2ae
Remove obsolete commented out lines
Flix6x Sep 8, 2022
99fe8de
Expand docstring of partition function
Flix6x Sep 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -8,6 +8,7 @@ v0.12.0 | October XX, 2022
New features
-------------

* Hit the replay button to replay what happened, available on the sensor and asset pages [see `PR #463 <http://www.github.com/FlexMeasures/flexmeasures/pull/463>`_]

Bugfixes
-----------
Expand Down
31 changes: 26 additions & 5 deletions flexmeasures/ui/static/js/replay-utils.js
@@ -1,23 +1,44 @@
// Replay utils
export function partition(array, callback){

/**
* Partitions array into two arrays.
*
* Partitions array into two array by pushing elements left or right given some decision function
* that is evaluated on each element.
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {Array} array Array to be partitioned.
* @param {function} decisionFunction Function that assigns elements to the left or right arrays.
* @return {Array} Array containing the left and right arrays.
*/
export function partition(array, decisionFunction){
return array.reduce(function(result, element, i) {
callback(element, i, array)
decisionFunction(element, i, array)
? result[0].push(element)
: result[1].push(element);
return result;
}, [[],[]]
);
};

/**
* Updates beliefs.
*
* Updates oldBeliefs with the most recent newBeliefs about the same event for the same sensor by the same source.
*
* @param {Array} oldBeliefs Array containing old beliefs.
* @param {Array} newBeliefs Array containing new beliefs.
* @return {Array} Array containing updated beliefs.
*/
export function updateBeliefs(oldBeliefs, newBeliefs) {
// Update oldBeliefs with the most recent newBeliefs about the same event for the same sensor by the same source

// Group by sensor, event start and source
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
var oldBeliefsByEventBySource = Object.fromEntries(new Map(oldBeliefs.map(belief => [belief.sensor.id + '_' + belief.event_start + '_' + belief.source.id, belief]))); // array -> dict (already had one belief per event)

// Group by sensor, event start and source, and select only the most recent new beliefs
var mostRecentNewBeliefsByEventBySource = Object.fromEntries(new Map(newBeliefs.map(belief => [belief.sensor.id + '_' + belief.event_start + '_' + belief.source.id, belief]))); // array -> dict (assumes beliefs are ordered by ascending belief time, with the last belief used as dict value)

// Return old beliefs updated with most recent new beliefs
return Object.values({...oldBeliefsByEventBySource, ...mostRecentNewBeliefsByEventBySource}) // dict -> list
return Object.values({...oldBeliefsByEventBySource, ...mostRecentNewBeliefsByEventBySource}) // dict -> array
}

// Define the step duration for the replay (value in ms)
export var beliefTimedelta = 3600000
58 changes: 27 additions & 31 deletions flexmeasures/ui/templates/base.html
Expand Up @@ -231,7 +231,7 @@
<script type="module" type="text/javascript">

import { subtract, thisMonth, lastNMonths, getOffsetBetweenTimezonesForDate } from "{{ url_for('flexmeasures_ui.static', filename='js/daterange-utils.js') }}";
import { partition, updateBeliefs } from "{{ url_for('flexmeasures_ui.static', filename='js/replay-utils.js') }}";
import { partition, updateBeliefs, beliefTimedelta} from "{{ url_for('flexmeasures_ui.static', filename='js/replay-utils.js') }}";

let vegaView;
let previousResult;
Expand Down Expand Up @@ -409,94 +409,90 @@
}
};

// Set up play/pause button for replay
// Set up play/pause button for replay, incl. the complete replay logic
let toggle = document.querySelector('#replay');
toggle.addEventListener('click', function(e) {
e.preventDefault();
// toggle.classList.toggle('paused');
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
if (toggle.classList.contains('playing')) {
// Pause replay
console.log("pause replay")
toggle.classList.remove('playing');
toggle.classList.add('paused');
} else if (toggle.classList.contains('stopped')) {
// Start replay
console.log("start replay")
// toggle.classList.remove('paused');
toggle.classList.remove('stopped');
toggle.classList.add('playing');
var beliefTime = new Date(storeStartDate)
// var beliefTimedelta = 900000
var beliefTimedelta = 3600000
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
// 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();
load(result);
replayBeliefsData(result[0]);
}).catch(console.error);

const timer = ms => new Promise(res => setTimeout(res, ms))

async function load (result) {
var remainingData = result[0]
var simulatedData = []
/**
* 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 queryBeliefTime = (beliefTime != null) ? (beliefTime.toISOString()) : (null)
beliefTime = new Date(beliefTime.getTime() + beliefTimedelta)

let t0= performance.now();

// Split off one simulation step of new data from the remaining data
// 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 simulated data given the new data
const oldLength = simulatedData.length + newData.length;
simulatedData = updateBeliefs(simulatedData, newData)
const newLength = simulatedData.length;
console.log("Removed " + (oldLength - newLength) + " obsolete beliefs")

let t1= performance.now();

console.log('Time taken to update data:'+ (t1-t0) +' milliseconds');

// todo: remove obsolete beliefs and insert most recent new beliefs
vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(simulatedData)).run().finalize();
// Update beliefs in the replayed data given the new data
replayedData = updateBeliefs(replayedData, newData)

let t2= performance.now();
/** 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)).run().finalize();

console.log('Time taken to embed data:'+ (t2-t1) +' milliseconds');
document.getElementById('replay-time').innerHTML = beliefTime
await timer(25);
}
// Stop replay when finished
console.log("stop replay")
toggle.classList.remove('playing');
toggle.classList.add('stopped');
document.getElementById('replay-time').innerHTML = ''
}
} else {
// Resume replay
console.log("resume replay")
toggle.classList.remove('paused');
toggle.classList.add('playing');
}
Expand Down