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 435 display sensor charts on asset page #449

Merged
merged 96 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
76499dc
Show updated sensor data and annotations together
Flix6x Jun 20, 2022
51aff4e
Show spinner while fetching new data
Flix6x Jun 20, 2022
bf6c5c7
Switch from id-based styling to class-based styling
Flix6x Jun 20, 2022
9fd91f1
Move styling to css, and lower spinner
Flix6x Jun 20, 2022
546cb65
Simplify and streamline datepicker fontsize
Flix6x Jun 20, 2022
39e3cef
Streamline datepicker margins
Flix6x Jun 20, 2022
4dae1a0
Add margins and side panel activated on hover
Flix6x Jun 20, 2022
f64b6e5
Correct margins and padding of side panel to allow for custom ranges …
Flix6x Jun 20, 2022
cf13a78
Show single month
Flix6x Jun 20, 2022
6939384
Fewer custom ranges
Flix6x Jun 20, 2022
8fb6406
Side panel rounded similar to buttons rather than similar to cards
Flix6x Jun 20, 2022
1ccfcc9
Align box shadows of cards and calendar
Flix6x Jun 20, 2022
37521cc
Non-transparent cards
Flix6x Jun 20, 2022
48e36e9
Simplified padding notation
Flix6x Jun 20, 2022
5d3db6e
Move chart actions buttons away from the card's corner (negative margin)
Flix6x Jun 20, 2022
6384e24
Rotate y-axis labels to improve legibility
Flix6x Jun 20, 2022
c6478e1
Remove sensor chart title if the same information is already containe…
Flix6x Jun 20, 2022
97b2bc9
Move unit to right side of tooltip
Flix6x Jun 21, 2022
c8e5dd5
Style predefined datetime ranges
Flix6x Jun 21, 2022
a12acf6
Raise column to top without requiring flex display
Flix6x Jun 21, 2022
2208986
Rename sidepanel class and separate styling specific to the sidepanel…
Flix6x Jun 21, 2022
50fb0d5
Show spinner only while the promise is being fulfilled
Flix6x Jun 21, 2022
ff62e43
Cancel previous request when the user makes a new request
Flix6x Jun 21, 2022
2c9c0ad
Do not let spinner block the full page height, so the sensor table na…
Flix6x Jun 21, 2022
e266ba9
Change header and label colors inside the sidepanel to contrast again…
Flix6x Jun 21, 2022
fb3d9e0
Style navbar logo to have a consistent height and adjust the width of…
Flix6x Jun 22, 2022
20726b2
Actually load intended font
Flix6x Jun 22, 2022
e650208
Enforce separation of time axis labels
Flix6x Jun 22, 2022
0fe334e
Add return type annotation and docs: applying chart defaults returns …
Flix6x Jun 22, 2022
b25d23d
Resolve hover glitch when exiting either the list of months or the li…
Flix6x Jun 23, 2022
1370f86
Enable swiping for left sidepanel
Flix6x Jun 23, 2022
9d8d627
Stop using redundant litepicker plugin, which was messing with calend…
Flix6x Jun 23, 2022
d21cf3f
Fix test
Flix6x Jun 20, 2022
a6c1c9b
Changelog entry
Flix6x Jun 23, 2022
241ef02
Synchronize styling of user pages
Flix6x Jun 23, 2022
7aaef2b
Style header action buttons
Flix6x Jun 23, 2022
be45711
Fix test
Flix6x Jun 23, 2022
b4b8bd7
Allow passing a default to get_attribute
Flix6x Jun 23, 2022
ec351f8
Add timezone and timerange properties to asset
Flix6x Jun 23, 2022
2cbbea7
Expose asset properties pertaining to its sensor data as a new dev AP…
Flix6x Jun 23, 2022
979f637
Speed up one case of belief searches
Flix6x Jun 23, 2022
2c59f48
Move loading of js scripts to base.html
Flix6x Jun 23, 2022
046574a
Add chart specs for showing multiple sensors
Flix6x Jun 23, 2022
145e4f3
Add methods to search beliefs for an asset's sensors and for creating…
Flix6x Jun 23, 2022
ae61779
Add endpoints to retrieve an asset chart and chart data
Flix6x Jun 23, 2022
56ceab6
Make sensor data a first class citizen of the asset page
Flix6x Jun 23, 2022
e873d60
Less margin around sensor listing on asset page
Flix6x Jun 23, 2022
aa4211e
black and flake8
Flix6x Jun 23, 2022
33b36c7
Fix test
Flix6x Jun 23, 2022
3b9eca9
Use join instead of concat in order to handle non-unique join keys; t…
Flix6x Jun 24, 2022
4e18332
Refactor: avoid redundant join and filter in case account_name is None
Flix6x Jun 24, 2022
e6fd7f5
Future annotations
Flix6x Jun 24, 2022
e87e5c9
Allow showing public sensors, too
Flix6x Jun 24, 2022
bcdf944
Fix sidepanel text color for small screens
Flix6x Jun 24, 2022
48d4e96
Add clickable sidepanel labels
Flix6x Jun 24, 2022
7354559
Refactor sidepanel script to base
Flix6x Jun 24, 2022
59fd47f
Keep all indices, instead of just those of the first sensor
Flix6x Jun 24, 2022
e8a60ff
Resample to the smallest resolution
Flix6x Jun 24, 2022
e4f27d5
Expand chart descriptions
Flix6x Jun 24, 2022
f64b87f
Rename default asset chart
Flix6x Jun 24, 2022
1019905
Increase default height of vertically concatenated charts
Flix6x Jun 24, 2022
3f2f5cb
Show tooltip of the nearest data point when hovering over the chart
Flix6x Jun 24, 2022
cb521e0
Remove unused transform
Flix6x Jun 24, 2022
5c2f89d
Add line layer
Flix6x Jun 24, 2022
439f0f1
Sort sensors by the order given in sensor_ids, and avoid double senso…
Flix6x Jul 5, 2022
83d1a22
Make sensors_to_show an asset property, and avoid getting redundant c…
Flix6x Jul 5, 2022
d9d5f06
Refactor
Flix6x Jul 5, 2022
9f6c860
Pass sensors to show if requesting a chart including data
Flix6x Jul 5, 2022
ba1d982
Add timezone warning
Flix6x Jul 5, 2022
c1e9423
Cannot compute minimum event resolution without data
Flix6x Jul 5, 2022
72fd34d
Fix the with_appcontext_if_needed decorator in case `flexmeasures run…
Flix6x Jul 6, 2022
040ff2e
Only start spinner upon loading data, not upon loading the page or th…
Flix6x Jul 6, 2022
6dd849b
Don't bother initializing the picker to specific dates in case of non…
Flix6x Jul 6, 2022
6ce3346
Make timescale axis match the requested date range, by updating the c…
Flix6x Jul 8, 2022
8cb8474
Use separate traces for each source, color by source name, and add so…
Flix6x Jul 8, 2022
fbd79e8
Avoid requiring new dependency
Flix6x Jul 8, 2022
f7e00b4
Show previous results while waiting for new results
Flix6x Jul 8, 2022
bd4aa4a
Fix calendar tooltip visibility
Flix6x Jul 8, 2022
7192ced
Custom date range styling in line with other button groups
Flix6x Jul 8, 2022
78b432c
Apply default legend font size also to layered views like vertically …
Flix6x Jul 8, 2022
ce3f5fb
Sync legend position on sensor and asset page, and allow longer labels
Flix6x Jul 8, 2022
94d4999
Fix merge_vega_lite_specs
Flix6x Jul 8, 2022
b8a6767
Rename asset attribute
Flix6x Jul 8, 2022
d5ac7ae
Changelog entries
Flix6x Jul 8, 2022
7fdab41
Merge remote-tracking branch 'origin/main' into Issue-435_Display_sen…
Flix6x Jul 11, 2022
6faf77d
Show tooltips using custom layer instead of voronoi
Flix6x Jul 11, 2022
a4cb5b3
Use shared smallest resolution rather than original sensor resolution
Flix6x Jul 11, 2022
02fd979
Fix search_beliefs with significant speed-up
Flix6x Jul 11, 2022
69e9514
Fix missing variable
Flix6x Jul 11, 2022
d25b27e
Workaround for timely-beliefs issue #104
Flix6x Jul 11, 2022
9b89134
Slice previous results to avoid a poorly scaled x-axis when going fro…
Flix6x Jul 11, 2022
623ac97
Do not show custom layer for NaN data
Flix6x Jul 11, 2022
2f787a6
Merge remote-tracking branch 'origin/main' into Issue-435_Display_sen…
Flix6x Jul 12, 2022
4132ed0
black
Flix6x Jul 12, 2022
fa377b6
Base timerange on sensors to show only
Flix6x Jul 12, 2022
dd7439b
mypy
Flix6x Jul 12, 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
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Expand Up @@ -7,6 +7,7 @@ v0.11.0 | June XX, 2022

New features
-------------
* The asset page now shows the most relevant sensor data for the asset [see `PR #449 <http://www.github.com/FlexMeasures/flexmeasures/pull/449>`_]
* Individual sensor charts show available annotations [see `PR #428 <http://www.github.com/FlexMeasures/flexmeasures/pull/428>`_]
* Collapsible sidepanel (hover/swipe) used for date selection on sensor charts, and various styling improvements [see `PR #447 <http://www.github.com/FlexMeasures/flexmeasures/pull/447>`_ and `PR #448 <http://www.github.com/FlexMeasures/flexmeasures/pull/448>`_]
* Switched from 12-hour AM/PM to 24-hour clock notation for time series chart axis labels [see `PR #446 <http://www.github.com/FlexMeasures/flexmeasures/pull/446>`_]
Expand All @@ -15,6 +16,7 @@ New features
Bugfixes
-----------
* Do not fail asset page if entity addresses cannot be built [see `PR #457 <http://www.github.com/FlexMeasures/flexmeasures/pull/457>`_]
* Time scale axes in sensor data charts now match the requested date range, rather than stopping at the edge of the available data [see `PR #449 <http://www.github.com/FlexMeasures/flexmeasures/pull/449>`_]

Infrastructure / Support
----------------------
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/api/dev/__init__.py
Expand Up @@ -5,7 +5,9 @@ def register_at(app: Flask):
"""This can be used to register FlaskViews."""

from flexmeasures.api.dev.sensors import SensorAPI
from flexmeasures.api.dev.sensors import AssetAPI

dev_api_prefix = "/api/dev"

SensorAPI.register(app, route_prefix=dev_api_prefix)
AssetAPI.register(app, route_prefix=dev_api_prefix)
32 changes: 30 additions & 2 deletions flexmeasures/api/dev/sensors.py
Expand Up @@ -9,8 +9,13 @@

from flexmeasures.auth.policy import ADMIN_ROLE, ADMIN_READER_ROLE
from flexmeasures.auth.decorators import permission_required_for_context
from flexmeasures.data.schemas.sensors import SensorIdField
from flexmeasures.data.schemas.times import AwareDateTimeField, DurationField
from flexmeasures.data.schemas import (
AssetIdField,
AwareDateTimeField,
DurationField,
SensorIdField,
)
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.services.annotations import prepare_annotations_for_chart

Expand Down Expand Up @@ -130,6 +135,29 @@ def get(self, id: int, sensor: Sensor):
return {attr: getattr(sensor, attr) for attr in attributes}


class AssetAPI(FlaskView):
"""
This view exposes asset attributes through API endpoints under development.
These endpoints are not yet part of our official API, but support the FlexMeasures UI.
"""

route_base = "/asset"

@route("/<id>/")
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
@permission_required_for_context("read", arg_name="asset")
def get(self, id: int, asset: GenericAsset):
"""GET from /asset/<id>

.. :quickref: Chart; Download asset attributes for use in charts
"""
attributes = ["name", "timezone", "timerange_of_sensors_to_show"]
return {attr: getattr(asset, attr) for attr in attributes}


def get_sensor_or_abort(id: int) -> Sensor:
"""
Util function to help the GET requests. Will be obsolete..
Expand Down
55 changes: 55 additions & 0 deletions flexmeasures/api/v3_0/assets.py
@@ -1,12 +1,16 @@
import json

from flask import current_app
from flask_classful import FlaskView, route
from flask_json import as_json
from marshmallow import fields
from webargs.flaskparser import use_kwargs, use_args

from flexmeasures.auth.decorators import permission_required_for_context
from flexmeasures.data import db
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.data.schemas import AwareDateTimeField
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema as AssetSchema
from flexmeasures.api.common.schemas.generic_assets import AssetIdField
from flexmeasures.api.common.schemas.users import AccountIdField
Expand Down Expand Up @@ -231,3 +235,54 @@ def delete(self, id: int, asset: GenericAsset):
db.session.commit()
current_app.logger.info("Deleted asset '%s'." % asset_name)
return {}, 204

@route("/<id>/chart/")
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
@use_kwargs(
{
"event_starts_after": AwareDateTimeField(format="iso", required=False),
"event_ends_before": AwareDateTimeField(format="iso", required=False),
"beliefs_after": AwareDateTimeField(format="iso", required=False),
"beliefs_before": AwareDateTimeField(format="iso", required=False),
"include_data": fields.Boolean(required=False),
"dataset_name": fields.Str(required=False),
"height": fields.Str(required=False),
"width": fields.Str(required=False),
},
location="query",
)
@permission_required_for_context("read", arg_name="asset")
def get_chart(self, id: int, asset: GenericAsset, **kwargs):
"""GET from /assets/<id>/chart

.. :quickref: Chart; Download a chart with time series
"""
return json.dumps(asset.chart(**kwargs))

@route("/<id>/chart_data/")
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
@use_kwargs(
{
"event_starts_after": AwareDateTimeField(format="iso", required=False),
"event_ends_before": AwareDateTimeField(format="iso", required=False),
"beliefs_after": AwareDateTimeField(format="iso", required=False),
"beliefs_before": AwareDateTimeField(format="iso", required=False),
},
location="query",
)
@permission_required_for_context("read", arg_name="asset")
def get_chart_data(self, id: int, asset: GenericAsset, **kwargs):
"""GET from /assets/<id>/chart_data

.. :quickref: Chart; Download time series for use in charts

Data for use in charts (in case you have the chart specs already).
"""
sensors = asset.sensors_to_show
return asset.search_beliefs(sensors=sensors, as_json=True, **kwargs)
167 changes: 161 additions & 6 deletions flexmeasures/data/models/charts/belief_charts.py
@@ -1,9 +1,15 @@
from __future__ import annotations

from datetime import datetime

from flexmeasures.data.models.charts.defaults import FIELD_DEFINITIONS
from flexmeasures.utils.flexmeasures_inflection import capitalize


def bar_chart(
sensor: "Sensor", # noqa F821
event_starts_after: datetime | None = None,
event_ends_before: datetime | None = None,
**override_chart_specs: dict,
):
unit = sensor.unit if sensor.unit else "a.u."
Expand All @@ -14,32 +20,181 @@ def bar_chart(
stack=None,
**FIELD_DEFINITIONS["event_value"],
)
event_start_field_definition = FIELD_DEFINITIONS["event_start"]
if event_starts_after and event_ends_before:
event_start_field_definition["scale"] = {
"domain": [
event_starts_after.timestamp() * 10**3,
event_ends_before.timestamp() * 10**3,
]
}
resolution_in_ms = sensor.event_resolution.total_seconds() * 1000
chart_specs = {
"description": "A simple bar chart.",
"description": "A simple bar chart showing sensor data.",
"title": capitalize(sensor.name) if sensor.name != sensor.sensor_type else None,
"mark": "bar",
"mark": {
"type": "bar",
"clip": True,
},
"encoding": {
"x": FIELD_DEFINITIONS["event_start"],
"x": event_start_field_definition,
"x2": FIELD_DEFINITIONS["event_end"],
"y": event_value_field_definition,
"color": FIELD_DEFINITIONS["source"],
"color": FIELD_DEFINITIONS["source_name"],
"detail": FIELD_DEFINITIONS["source"],
"opacity": {"value": 0.7},
"tooltip": [
FIELD_DEFINITIONS["full_date"],
{
**event_value_field_definition,
**dict(title=f"{capitalize(sensor.sensor_type)}"),
},
FIELD_DEFINITIONS["source"],
FIELD_DEFINITIONS["source_name"],
FIELD_DEFINITIONS["source_model"],
],
},
"transform": [
{
"calculate": f"datum.event_start + {sensor.event_resolution.total_seconds() * 1000}",
"calculate": f"datum.event_start + {resolution_in_ms}",
"as": "event_end",
},
],
}
for k, v in override_chart_specs.items():
chart_specs[k] = v
return chart_specs


def chart_for_multiple_sensors(
sensors: list["Sensor"], # noqa F821
event_starts_after: datetime | None = None,
event_ends_before: datetime | None = None,
**override_chart_specs: dict,
):
sensors_specs = []
min_resolution_in_ms = (
min(sensor.event_resolution for sensor in sensors).total_seconds() * 1000
)
for sensor in sensors:
unit = sensor.unit if sensor.unit else "a.u."
event_value_field_definition = dict(
title=f"{capitalize(sensor.sensor_type)} ({unit})",
format=[".3s", unit],
formatType="quantityWithUnitFormat",
stack=None,
**{
**FIELD_DEFINITIONS["event_value"],
**dict(field=sensor.id),
},
)
event_start_field_definition = FIELD_DEFINITIONS["event_start"]
if event_starts_after and event_ends_before:
event_start_field_definition["scale"] = {
"domain": [
event_starts_after.timestamp() * 10**3,
event_ends_before.timestamp() * 10**3,
]
}
shared_tooltip = [
FIELD_DEFINITIONS["full_date"],
{
**event_value_field_definition,
**dict(title=f"{capitalize(sensor.sensor_type)}"),
},
FIELD_DEFINITIONS["source_name"],
FIELD_DEFINITIONS["source_model"],
]
sensor_specs = {
"title": capitalize(sensor.name)
if sensor.name != sensor.sensor_type
else None,
"layer": [
{
"mark": {
"type": "line",
"interpolate": "step-after",
"clip": True,
},
"encoding": {
"x": event_start_field_definition,
"y": event_value_field_definition,
"color": FIELD_DEFINITIONS["source_name"],
"detail": FIELD_DEFINITIONS["source"],
},
},
{
"mark": {
"type": "rect",
"y2": "height",
"opacity": 0,
},
"encoding": {
"x": event_start_field_definition,
"x2": FIELD_DEFINITIONS["event_end"],
"y": {
"condition": {
"test": "isNaN(datum['event_value'])",
**event_value_field_definition,
},
"value": 0,
},
"detail": FIELD_DEFINITIONS["source"],
"tooltip": shared_tooltip,
},
"transform": [
{
"calculate": f"datum.event_start + {min_resolution_in_ms}",
"as": "event_end",
},
],
},
{
"mark": {
"type": "circle",
"opacity": 1,
"clip": True,
},
"encoding": {
"x": event_start_field_definition,
"y": event_value_field_definition,
"color": FIELD_DEFINITIONS["source_name"],
"detail": FIELD_DEFINITIONS["source"],
"size": {
"condition": {
"value": "200",
"test": {"param": "paintbrush", "empty": False},
},
"value": "0",
},
"tooltip": shared_tooltip,
},
"params": [
{
"name": "paintbrush",
"select": {
"type": "point",
"encodings": ["x"],
"on": "mouseover",
"nearest": False,
},
},
],
},
],
"width": "container",
}
sensors_specs.append(sensor_specs)
chart_specs = dict(
description="A vertically concatenated chart showing sensor data.",
vconcat=[*sensors_specs],
spacing=100,
bounds="flush",
)
chart_specs["config"] = {
"view": {"continuousWidth": 800, "continuousHeight": 150},
"autosize": {"type": "fit-x", "contains": "padding"},
}
chart_specs["resolve"] = {"scale": {"x": "shared"}}
for k, v in override_chart_specs.items():
chart_specs[k] = v
return chart_specs