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

Issue 502 visually distinguish forecasts from measurements #503

Merged
Merged
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -9,6 +9,7 @@ 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>`_]
* Visually distinguish forecasts/schedules (dashed lines) from measurements (solid lines), and expand the tooltip with timing info regarding the forecast/schedule horizon or measurement lag [see `PR #503 <http://www.github.com/FlexMeasures/flexmeasures/pull/503>`_]
* Improved import of time series data from CSV file: 1) drop duplicate records with warning, and 2) allow configuring which column contains explicit recording times for each data point (use case: import forecasts) [see `PR #501 <http://www.github.com/FlexMeasures/flexmeasures/pull/501>`_]

Bugfixes
Expand Down
58 changes: 43 additions & 15 deletions flexmeasures/data/models/charts/belief_charts.py
Expand Up @@ -99,34 +99,62 @@ def chart_for_multiple_sensors(
}
shared_tooltip = [
FIELD_DEFINITIONS["full_date"],
dict(
field="belief_horizon",
type="quantitative",
title="Horizon",
format=["d", 4],
formatType="timedeltaFormat",
),
{
**event_value_field_definition,
**dict(title=f"{capitalize(sensor.sensor_type)}"),
},
FIELD_DEFINITIONS["source_name"],
FIELD_DEFINITIONS["source_model"],
]
line_layer = {
"mark": {
"type": "line",
"interpolate": "step-after"
if sensor.event_resolution != timedelta(0)
else "linear",
"clip": True,
},
"encoding": {
"x": event_start_field_definition,
"y": event_value_field_definition,
"color": FIELD_DEFINITIONS["source_name"],
"strokeDash": {
"condition": {
"test": "datum['belief_horizon'] > 0",
"value": [1, 2], # dashed
},
"value": [1, 0], # solid
},
"detail": FIELD_DEFINITIONS["source"],
},
}
ex_ante_line_layer = {
**line_layer,
**{
"transform": [{"filter": "datum.belief_horizon > 0"}],
},
}
ex_post_line_layer = {
**line_layer,
**{
"transform": [{"filter": "datum.belief_horizon <= 0"}],
},
}
sensor_specs = {
"title": capitalize(sensor.name)
if sensor.name != sensor.sensor_type
else None,
"transform": [{"filter": f"datum.sensor.id == {sensor.id}"}],
"layer": [
{
"mark": {
"type": "line",
"interpolate": "step-after"
if sensor.event_resolution != timedelta(0)
else "linear",
"clip": True,
},
"encoding": {
"x": event_start_field_definition,
"y": event_value_field_definition,
"color": FIELD_DEFINITIONS["source_name"],
"detail": FIELD_DEFINITIONS["source"],
},
},
ex_ante_line_layer,
ex_post_line_layer,
{
"mark": {
"type": "rect",
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/data/models/generic_assets.py
Expand Up @@ -394,6 +394,7 @@ def search_beliefs(
for sensor, bdf in bdf_dict.items():
if bdf.event_resolution > timedelta(0):
bdf = bdf.resample_events(minimum_non_zero_resolution)
bdf["belief_horizon"] = bdf.belief_horizons.to_numpy()
df = simplify_index(
bdf,
index_levels_to_columns=["source"]
Expand Down
30 changes: 23 additions & 7 deletions flexmeasures/ui/static/js/flexmeasures.js
Expand Up @@ -311,13 +311,29 @@ function submit_sensor_type() {
$("#sensor_type-form").attr("action", empty_location).submit();
}

/** Tooltips: Register custom formatter for quantities incl. units
Usage:
{
'format': [<d3-format>, <sensor unit>],
'formatType': 'quantityWithUnitFormat'
}
*/
/** Tooltips: Register custom formatters
*
* - Quantities incl. units
* Usage:
* {
* 'format': [<d3-format>, <sensor unit>],
* 'formatType': 'quantityWithUnitFormat'
* }
* - Timedeltas measured in human-readable quantities (usually not milliseconds)
* Usage:
* {
* 'format': [<d3-format>, <breakpoint>],
* 'formatType': 'timedeltaFormat'
* }
*/
vega.expressionFunction('quantityWithUnitFormat', function(datum, params) {
return d3.format(params[0])(datum) + " " + params[1];
});
vega.expressionFunction('timedeltaFormat', function(timedelta, params) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document here what I had to find out by myself?

params[0] is a D3 format identifier (.e.g "d" for days)
params[1] is a multiplier which decides from how many X on we format as Y (e.g. <=3 days means we format as "hours")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, d stands for decimal notation, rounded to integer. It's a d3-format rather than a d3-time-format. I'm expanding the inline documentation.

return (Math.abs(timedelta) > 1000 * 60 * 60 * 24 * 365.2425 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60 * 24 * 365.2425)) + " years"
: Math.abs(timedelta) > 1000 * 60 * 60 * 24 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60 * 24)) + " days"
: Math.abs(timedelta) > 1000 * 60 * 60 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60 * 60)) + " hours"
: Math.abs(timedelta) > 1000 * 60 * params[1] ? d3.format(params[0])(timedelta / (1000 * 60)) + " minutes"
: Math.abs(timedelta) > 1000 * params[1] ? d3.format(params[0])(timedelta / 1000) + " seconds"
: d3.format(params[0])(timedelta) + " milliseconds");
});