WIP client filter
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -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 <a target=\"_top\" rel=\"noopener\" href=\"https://tile.openstreetmap.org/\">OpenStreetMap tile servers</a>, under the <a target=\"_top\" rel=\"noopener\" href=\"https://operations.osmfoundation.org/policies/tiles/\">tile usage policy</a>. Data by <a target=\"_top\" rel=\"noopener\" href=\"http://openstreetmap.org\">OpenStreetMap</a>"
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
"glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "osm",
|
||||
"type": "raster",
|
||||
"source": "osm"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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/<string:vin>/<int:charge>')
|
||||
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([
|
||||
|
||||