From c31b3f4854ed32338f4c62c7f7cddf5e35c7968a Mon Sep 17 00:00:00 2001 From: Florian Bezannier Date: Wed, 5 May 2021 20:36:51 +0200 Subject: [PATCH] WIP client filter --- trip.py | 12 +++- web/app.py | 6 +- web/assets/clientside.js | 48 +++++++++++++ .../images/battery-charge-line.svg | 0 web/{static => assets}/images/consumption.svg | 0 .../images/electricity bill.svg | 0 web/{static => assets}/images/pollution.svg | 0 web/assets/style.json | 22 ++++++ web/figures.py | 71 +++++++++---------- web/views.py | 59 +++++++++++---- 10 files changed, 165 insertions(+), 53 deletions(-) create mode 100644 web/assets/clientside.js rename web/{static => assets}/images/battery-charge-line.svg (100%) rename web/{static => assets}/images/consumption.svg (100%) rename web/{static => assets}/images/electricity bill.svg (100%) rename web/{static => assets}/images/pollution.svg (100%) create mode 100644 web/assets/style.json diff --git a/trip.py b/trip.py index 207e1a9..8c6208e 100644 --- a/trip.py +++ b/trip.py @@ -89,8 +89,8 @@ class Trip: return res def get_consumption(self): - return {"speed": self.speed_average, "consumption_km": self.consumption_km, "date": self.start_at, - "consumption_by_temp": self.get_temperature()} + return {"speed": self.speed_average, "consumption_km": self.consumption_km, "start_at": self.start_at, + "consumption_by_temp": self.get_temperature(), "positions": self.get_positions()} def set_altitude_diff(self, start, end): try: @@ -98,6 +98,14 @@ class Trip: except (NameError, TypeError): pass + def get_positions(self): + lat = [] + long = [] + for position in self.positions: + lat.append(position.latitude) + long.append(position.longitude) + return {"lat": lat, "long": long} + class Trips(list): def __init__(self, *args): diff --git a/web/app.py b/web/app.py index 9c7aebf..4b3bb2f 100644 --- a/web/app.py +++ b/web/app.py @@ -27,7 +27,8 @@ myp: MyPSACC = None chc: ChargeControls = None -def start_app(title, base_path, debug: bool, host, port, reloader=False): # pylint: disable=too-many-arguments +def start_app(title, base_path, debug: bool, host, port, reloader=False, # pylint: disable=too-many-arguments + unminified=False): global app, dash_app, dispatcher try: lang = locale.getlocale()[0].split("_")[0] @@ -36,6 +37,8 @@ def start_app(title, base_path, debug: bool, host, port, reloader=False): # pyl except (IndexError, locale.Error): locale_url = None logger.warning("Can't get language") + if unminified: + locale_url = ["assets/plotly-with-meta.js"] app = Flask(__name__) app.config["DEBUG"] = debug if base_path == "/": @@ -46,6 +49,7 @@ def start_app(title, base_path, debug: bool, host, port, reloader=False): # pyl requests_pathname_prefix = base_path + "/" dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title=title, server=app, requests_pathname_prefix=requests_pathname_prefix) + dash_app.enable_dev_tools(reloader) # keep this line import web.views # pylint: disable=unused-import,import-outside-toplevel return run_simple(host, port, application, use_reloader=reloader, use_debugger=debug) diff --git a/web/assets/clientside.js b/web/assets/clientside.js new file mode 100644 index 0000000..9c292d4 --- /dev/null +++ b/web/assets/clientside.js @@ -0,0 +1,48 @@ +function filter_dataset(data, range, old_figure, x,y) { + function is_in_range(st){ + ts_date = new Date(st).getTime()/1000 + return ts_date >= range[0] && ts_date <= range[1] + } + var data_filtered = data.filter(line => is_in_range(line["start_at"])); + var figures = []; + var i=0 + y.forEach(function (y_label){ + var x_label=x[i] + var figure = Object.assign({}, old_figure[i]); + i++; + // var unique_y_label = y[i].filter((v, i, a) => a.indexOf(v) === i); + //var data_nonnull = data_filtered + // unique_y_label.forEach(function(label) { + // data_nonnull = data_nonnull.filter(line => line[label]); + // }); + if ("mapbox" in figure["layout"]){ + figure["data"][0]["lat"] = [] + figure["data"][0]["lon"] = [] + figure["data"][0]["hovertext"] = [] + var trip = null; + for (trip of data_filtered) { + x_pos = trip["positions"][x_label] + figure["data"][0]["lat"].push(...x_pos, null); + figure["data"][0]["lon"].push(...trip["positions"][y_label[0]]); + figure["data"][0]["hovertext"].push(...Array(x_pos.length).fill(trip[y_label[1]]), null); + } + var last_pos = trip["positions"][y_label[0]].length; + figure.layout.mapbox.center.lat = trip["positions"][x_label][last_pos - 1]; + figure.layout.mapbox.center.lon = trip["positions"][y_label[0]][last_pos - 1]; + figure.data[1].lat = [figure.layout.mapbox.center.lat] + figure.data[1].lon = [figure.layout.mapbox.center.lon] + } + else { + x_values = data_filtered.map(a => a[x_label]) + // for each y label + for (j = 0; j < y_label.length; j++) { + figure["data"][j]["y"] = data_filtered.map(a => a[y_label[j]]); + figure["data"][j]["x"] = x_values + } + } + console.log(figure); + figures.push(figure); + + }); + return figures; +} diff --git a/web/static/images/battery-charge-line.svg b/web/assets/images/battery-charge-line.svg similarity index 100% rename from web/static/images/battery-charge-line.svg rename to web/assets/images/battery-charge-line.svg diff --git a/web/static/images/consumption.svg b/web/assets/images/consumption.svg similarity index 100% rename from web/static/images/consumption.svg rename to web/assets/images/consumption.svg diff --git a/web/static/images/electricity bill.svg b/web/assets/images/electricity bill.svg similarity index 100% rename from web/static/images/electricity bill.svg rename to web/assets/images/electricity bill.svg diff --git a/web/static/images/pollution.svg b/web/assets/images/pollution.svg similarity index 100% rename from web/static/images/pollution.svg rename to web/assets/images/pollution.svg diff --git a/web/assets/style.json b/web/assets/style.json new file mode 100644 index 0000000..1e9b3ed --- /dev/null +++ b/web/assets/style.json @@ -0,0 +1,22 @@ +{ + "version": 8, + "sources": { + "osm": { + "type": "raster", + "tiles": [ + "https://tile.openstreetmap.org/{z}/{x}/{y}.png" + ], + "tileSize": 256, + "attribution": "Map tiles by OpenStreetMap tile servers, under the tile usage policy. Data by OpenStreetMap" + } + }, + "sprite": "", + "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "osm", + "type": "raster", + "source": "osm" + } + ] +} diff --git a/web/figures.py b/web/figures.py index f23110a..ea2de9a 100644 --- a/web/figures.py +++ b/web/figures.py @@ -1,10 +1,10 @@ from copy import deepcopy +from statistics import mean from typing import List import dash_bootstrap_components as dbc import dash_table -import numpy as np from dash_core_components import Graph from dash_table.Format import Format, Scheme, Symbol from dateutil.relativedelta import relativedelta @@ -55,37 +55,34 @@ consumption_fig = ERROR_DIV consumption_df = ERROR_DIV trips_map = ERROR_DIV consumption_fig_by_speed = ERROR_DIV -consumption_graph_by_temp = ERROR_DIV +consumption_fig_by_temp = ERROR_DIV table_fig = ERROR_DIV pandas_options.display.float_format = '${:.2f}'.format info = "" battery_info = ERROR_DIV battery_table = None +consumption_df_dict = None -SUMMARY_CARDS = {"Average consumption": {"text": None, "src": "static/images/consumption.svg"}, - "Average emission": {"text": None, "src": "static/images/pollution.svg"}, - "Average charge speed": {"text": None, "src": "static/images/battery-charge-line.svg"}, - "Electricity consumption": {"text": None, "src": "static/images/electricity bill.svg"} +SUMMARY_CARDS = {"Average consumption": {"text": None, "src": "assets/images/consumption.svg"}, + "Average emission": {"text": None, "src": "assets/images/pollution.svg"}, + "Average charge speed": {"text": None, "src": "assets/images/battery-charge-line.svg"}, + "Electricity consumption": {"text": None, "src": "assets/images/electricity bill.svg"} } # pylint: disable=too-many-locals def get_figures(trips: Trips, charging: List[dict]): global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, battery_info, \ - battery_table, consumption_graph_by_temp - lats = [] - lons = [] - names = [] - for trip in trips: - for points in trip.positions: - lats = np.append(lats, points.latitude) - lons = np.append(lons, points.longitude) - names = np.append(names, [str(trip.start_at)]) - lats = np.append(lats, None) - lons = np.append(lons, None) - names = np.append(names, None) - trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names, - mapbox_style="stamen-terrain", zoom=12) + battery_table, consumption_fig_by_temp, consumption_df_dict + lats = [42, 41] + lons = [1, 2] + names = ["undefined", "undefined"] + trips_map = px.line_mapbox(lat=lats, lon=lons, hover_name=names, zoom=12, mapbox_style="/assets/style2.json") + trips_map.add_trace(go.Scattermapbox( + mode="markers", + marker={"symbol": "marker", "size": 20}, + lon=[lons[0]], lat=[lats[0]], + showlegend=False, name="Last Position")) # table nb_format = Format(precision=2, scheme=Scheme.fixed, symbol=Symbol.yes) # pylint: disable=no-member table_fig = dash_table.DataTable( @@ -120,20 +117,19 @@ def get_figures(trips: Trips, charging: List[dict]): page_size=50 ) # consumption_fig - consumption_df = DataFrame.from_records(trips.get_long_trips()) - consumption_fig = px.histogram(consumption_df, x="date", y="consumption_km", title='Consumption of the car', + consumption_df_dict = trips.get_long_trips() + consumption_fig = px.histogram(x=[0], y=[1], title='Consumption of the car', histfunc="avg") consumption_fig.update_layout(yaxis_title="Consumption kWh/100Km") - consumption_fig_by_speed = px.histogram(consumption_df, x="speed", y="consumption_km", histfunc="avg", + consumption_fig_by_speed = px.histogram(x=[0], y=[1], histfunc="avg", title="Consumption by speed") consumption_fig_by_speed.update_traces(xbins_size=15) consumption_fig_by_speed.update_layout(bargap=0.05) - consumption_fig_by_speed.add_trace( - go.Scatter(mode="markers", x=consumption_df["speed"], y=consumption_df["consumption_km"], - name="Trips")) + consumption_fig_by_speed.add_trace(go.Scatter(mode="markers", x=[0], + y=[0], name="Trips")) consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km") - kw_per_km = float(consumption_df["consumption_km"].mean()) + kw_per_km = mean([t.consumption_km for t in trips]) info = "Average consumption: {:.1f} kWh/100km".format(kw_per_km) # charging @@ -154,7 +150,7 @@ def get_figures(trips: Trips, charging: List[dict]): SUMMARY_CARDS["Average emission"]["text"] = [html.P(f"{co2_per_km:.1f} g/km"), html.P(f"{co2_per_kw:.1f} g/kWh")] SUMMARY_CARDS["Electricity consumption"]["text"] = [f"{total_elec:.0f} kWh", html.Br(), \ f"{total_elec * price_kw:.0f} {ElecPrice.currency}"] - SUMMARY_CARDS["Average consumption"]["text"] = f"{consumption_df['consumption_km'].mean():.1f} kWh/100km" + SUMMARY_CARDS["Average consumption"]["text"] = f"{kw_per_km:.1f} kWh/100km" battery_table = dash_table.DataTable( id='battery-table', sort_action='native', @@ -183,21 +179,22 @@ def get_figures(trips: Trips, charging: List[dict]): } ], ) - consumption_by_temp_df = consumption_df[consumption_df["consumption_by_temp"].notnull()] - if len(consumption_by_temp_df) > 0: - consumption_fig_by_temp = px.histogram(consumption_by_temp_df, x="consumption_by_temp", y="consumption_km", + consumption_fig_by_temp = None + temp_value = False + for trip in trips: + if trip.get_temperature() is not None: + temp_value = True + break + if temp_value: + consumption_fig_by_temp = px.histogram(x=[0], y=[0], histfunc="avg", title="Consumption by temperature") consumption_fig_by_temp.update_traces(xbins_size=2) consumption_fig_by_temp.update_layout(bargap=0.05) consumption_fig_by_temp.add_trace( - go.Scatter(mode="markers", x=consumption_by_temp_df["consumption_by_temp"], - y=consumption_by_temp_df["consumption_km"], name="Trips")) + go.Scatter(mode="markers", x=[0], + y=[0], name="Trips")) consumption_fig_by_temp.update_layout(xaxis_title="average temperature in °C", yaxis_title="Consumption kWh/100Km") - consumption_graph_by_temp = html.Div(Graph(figure=consumption_fig_by_temp), id="consumption_graph_by_temp") - - else: - consumption_graph_by_temp = html.Div(Graph(style={'display': 'none'}), id="consumption_graph_by_temp") return True diff --git a/web/views.py b/web/views.py index 822f678..ad77ef4 100644 --- a/web/views.py +++ b/web/views.py @@ -55,14 +55,28 @@ def diff_dashtable(data, data_previous, row_id_name="row_id"): return changes +figures_list = ["consumption_fig", "consumption_fig_by_speed", "consumption_graph_by_temp", "trips_map"] +y_list = [["consumption_km"], ["consumption_km", "consumption_km"], ["consumption_km", "consumption_km"], + ["long", "start_at"]] +x_list = ["start_at", "speed", "consumption_by_temp", "lat"] +outputs = [Output(id, "figure") for id in figures_list] + + +dash_app.clientside_callback( + """ + function(data,range, figures) { + return filter_dataset(data,range,figures,%s, %s); + } + """ % (x_list, y_list), + *outputs, + Input('clientside-data-store', 'data'), + Input('date-slider', 'value'), + Input('clientside-figure-store', 'data')) + def create_callback(): # noqa: MC0001 global CALLBACK_CREATED if not CALLBACK_CREATED: - @dash_app.callback(Output('trips_map', 'figure'), - Output('consumption_fig', 'figure'), - Output('consumption_fig_by_speed', 'figure'), - Output('consumption_graph_by_temp', 'children'), - Output('summary-cards', 'children'), + @dash_app.callback(Output('summary-cards', 'children'), Output('tab_trips_fig', 'children'), Output('tab_charge', 'children'), Output('date-slider', 'max'), @@ -78,8 +92,7 @@ def create_callback(): # noqa: MC0001 filtered_trips.append(trip) filtered_chargings = Charging.get_chargings(mini, maxi) figures.get_figures(filtered_trips, filtered_chargings) - return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, \ - figures.consumption_graph_by_temp, create_card(figures.SUMMARY_CARDS), \ + return create_card(figures.SUMMARY_CARDS), \ figures.table_fig, figures.battery_table, max_millis, step, marks @dash_app.callback(Output(EMPTY_DIV, "children"), @@ -159,6 +172,21 @@ def get_vehicle_info(vin): return response +STYLE_CACHE = None + + +@app.route("/assets/style2.json") +def get_style(): + global STYLE_CACHE + if not STYLE_CACHE: + with open(app.root_path + "/assets/style.json", "r") as f: + res = json.loads(f.read()) + STYLE_CACHE = res + url_root = request.url_root + STYLE_CACHE["sprite"] = url_root + "assets/sprites/osm-liberty@2x" + return jsonify(STYLE_CACHE) + + @app.route('/charge_now//') def charge_now(vin, charge): return jsonify(myp.charge_now(vin, charge != 0)) @@ -312,9 +340,9 @@ def create_card(card: dict): dbc.Card([ html.H4(tile, className="card-title text-center"), dbc.Row([ - dbc.Col(dbc.CardBody(text, style={"white-space": "nowrap", "font-size": "160%"}), + dbc.Col(dbc.CardBody(text, style={"whiteS pace": "nowrap", "fontSize": "160%"}), className="text-center"), - dbc.Col(dbc.CardImg(src=value.get("src", Component.UNDEFINED), style={"max-height": "7rem"})) + dbc.Col(dbc.CardImg(src=value.get("src", Component.UNDEFINED), style={"maxHeight": "7rem"})) ], className="align-items-center flex-nowrap") ], className="h-100 p-2"), @@ -331,10 +359,12 @@ def serve_layout(): figures.get_figures(trips, chargings) summary_tab = [dbc.Container(dbc.Row(id="summary-cards", children=create_card(figures.SUMMARY_CARDS)), fluid=True), - dcc.Graph(figure=figures.consumption_fig, id="consumption_fig"), - dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed"), - figures.consumption_graph_by_temp] - maps = dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'}) + dcc.Graph(id="consumption_fig"), + dcc.Graph(id="consumption_fig_by_speed"), + dcc.Graph(id="consumption_graph_by_temp", + style={'display': 'none'} if figures.consumption_fig_by_temp is None else {}, + )] + maps = dcc.Graph(id="trips_map", style={"height": '90vh'}) create_callback() range_slider = dcc.RangeSlider( id='date-slider', @@ -350,6 +380,9 @@ def serve_layout(): logger.warning("Failed to generate figure, there is probably not enough data yet", exc_info_debug=True) range_slider = html.Div() data_div = html.Div([ + dcc.Store(id='clientside-figure-store', data=[figures.consumption_fig, figures.consumption_fig_by_speed, + figures.consumption_fig_by_temp, figures.trips_map]), + dcc.Store(id='clientside-data-store', data=figures.consumption_df_dict), range_slider, html.Div([ dbc.Tabs([