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 all 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
1 change: 1 addition & 0 deletions flexmeasures/api/dev/sensors.py
Expand Up @@ -71,6 +71,7 @@ def get_chart(self, id: int, sensor: Sensor, **kwargs):
"beliefs_after": AwareDateTimeField(format="iso", required=False),
"beliefs_before": AwareDateTimeField(format="iso", required=False),
"resolution": DurationField(required=False),
"most_recent_beliefs_only": fields.Boolean(required=False),
},
location="query",
)
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/api/v3_0/assets.py
Expand Up @@ -297,6 +297,7 @@ def get_chart(self, id: int, asset: GenericAsset, **kwargs):
"event_ends_before": AwareDateTimeField(format="iso", required=False),
"beliefs_after": AwareDateTimeField(format="iso", required=False),
"beliefs_before": AwareDateTimeField(format="iso", required=False),
"most_recent_beliefs_only": fields.Boolean(required=False),
},
location="query",
)
Expand Down
6 changes: 2 additions & 4 deletions flexmeasures/data/models/charts/belief_charts.py
Expand Up @@ -87,10 +87,7 @@ def chart_for_multiple_sensors(
format=[".3~r", unit],
formatType="quantityWithUnitFormat",
stack=None,
**{
**FIELD_DEFINITIONS["event_value"],
**dict(field=sensor.id),
},
**FIELD_DEFINITIONS["event_value"],
)
event_start_field_definition = FIELD_DEFINITIONS["event_start"]
if event_starts_after and event_ends_before:
Expand All @@ -113,6 +110,7 @@ def chart_for_multiple_sensors(
"title": capitalize(sensor.name)
if sensor.name != sensor.sensor_type
else None,
"transform": [{"filter": f"datum.sensor.id == {sensor.id}"}],
"layer": [
{
"mark": {
Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/data/models/data_sources.py
Expand Up @@ -81,10 +81,10 @@ def description(self):
descr += f" v{self.version}"
return descr

def __repr__(self):
def __repr__(self) -> str:
return "<Data source %r (%s)>" % (self.id, self.description)

def __str__(self):
def __str__(self) -> str:
return self.description

def to_dict(self) -> dict:
Expand Down
36 changes: 27 additions & 9 deletions flexmeasures/data/models/generic_assets.py
Expand Up @@ -342,6 +342,7 @@ def search_beliefs(
source: Optional[
Union[DataSource, List[DataSource], int, List[int], str, List[str]]
] = None,
most_recent_beliefs_only: bool = True,
most_recent_events_only: bool = False,
as_json: bool = False,
) -> Union[BeliefsDataFrame, str]:
Expand Down Expand Up @@ -373,7 +374,7 @@ def search_beliefs(
horizons_at_least=horizons_at_least,
horizons_at_most=horizons_at_most,
source=source,
most_recent_beliefs_only=True,
most_recent_beliefs_only=most_recent_beliefs_only,
most_recent_events_only=most_recent_events_only,
one_deterministic_belief_per_event_per_source=True,
)
Expand All @@ -394,18 +395,35 @@ def search_beliefs(
if bdf.event_resolution > timedelta(0):
bdf = bdf.resample_events(minimum_non_zero_resolution)
df = simplify_index(
bdf, index_levels_to_columns=["source"]
).set_index(["source"], append=True)
df_dict[sensor.id] = df.rename(columns=dict(event_value=sensor.id))
df = list(df_dict.values())[0].join(
list(df_dict.values())[1:], how="outer"
)
bdf,
index_levels_to_columns=["source"]
if most_recent_beliefs_only
else ["belief_time", "source"],
).set_index(
["source"]
if most_recent_beliefs_only
else ["belief_time", "source"],
append=True,
)
df["sensor"] = sensor # or some JSONfiable representation
df = df.set_index(["sensor"], append=True)
df_dict[sensor.id] = df
df = pd.concat(df_dict.values())
else:
df = simplify_index(
BeliefsDataFrame(), index_levels_to_columns=["source"]
).set_index(["source"], append=True)
BeliefsDataFrame(),
index_levels_to_columns=["source"]
if most_recent_beliefs_only
else ["belief_time", "source"],
).set_index(
["source"]
if most_recent_beliefs_only
else ["belief_time", "source"],
append=True,
)
df = df.reset_index()
df["source"] = df["source"].apply(lambda x: x.to_dict())
df["sensor"] = df["sensor"].apply(lambda x: x.to_dict())
return df.to_json(orient="records")
return bdf_dict

Expand Down
11 changes: 11 additions & 0 deletions flexmeasures/data/models/time_series.py
Expand Up @@ -339,6 +339,8 @@ def search_beliefs(
)
if as_json:
df = bdf.reset_index()
df["sensor"] = self
df["sensor"] = df["sensor"].apply(lambda x: x.to_dict())
df["source"] = df["source"].apply(lambda x: x.to_dict())
return df.to_json(orient="records")
return bdf
Expand Down Expand Up @@ -480,6 +482,15 @@ def timerange(self) -> Dict[str, datetime_type]:
def __repr__(self) -> str:
return f"<Sensor {self.id}: {self.name}, unit: {self.unit} res.: {self.event_resolution}>"

def __str__(self) -> str:
return self.name

def to_dict(self) -> dict:
return dict(
id=self.id,
name=self.name,
)

@classmethod
def find_closest(
cls, generic_asset_type_name: str, sensor_name: str, n: int = 1, **kwargs
Expand Down
51 changes: 50 additions & 1 deletion flexmeasures/ui/static/css/flexmeasures.css
Expand Up @@ -1770,4 +1770,53 @@ div.heading-group {
align-items: center;
}

/* ---- End Unsure / potential Legacy ---- */
/* ---- End Unsure / potential Legacy ---- */


/* ---- Play/Pause button ---- */
.replay-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#replay-time {
margin-top: 15px;
}
#replay {
cursor: pointer !important;
margin-top: 190px;
width: 50px;
height: 50px;
position: relative;
}
#replay:before, #replay:after {
content: "";
position: absolute;
width: 0;
border-style: solid;
border-color: transparent;
border-left-color: var(--secondary-color);
transition: 0.3s;
}
#replay:not(.playing):before {
height: 50px;
border-width: 25px 0px 25px 50px;
}
#replay:not(.playing):after {
left: 25px;
top: 12.5px;
height: 0;
border-width: 12.5px 0 12.5px 25px;
}
#replay:before {
left: 0;
height: 50px;
border-width: 0 0 0 16.6666666667px;
}
#replay:after {
left: 33.3333333333px;
top: 0px;
height: 50px;
border-width: 0 0 0 16.6666666667px;
}
44 changes: 44 additions & 0 deletions flexmeasures/ui/static/js/replay-utils.js
@@ -0,0 +1,44 @@
// Replay utils

/**
* Partitions array into two arrays.
*
* Partitions array into two array by pushing elements left or right given some decision function, which is
* evaluated on each element. Successful validations lead to placement on the left side, others on the right.
*
* @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) {
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) {
// 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 -> array
}

// Define the step duration for the replay (value in ms)
export var beliefTimedelta = 3600000