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([