geo-activity-playground 0.40.1__py3-none-any.whl → 0.42.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +70 -0
- geo_activity_playground/alembic/versions/script.py.mako +0 -6
- geo_activity_playground/core/activities.py +21 -44
- geo_activity_playground/core/datamodel.py +121 -60
- geo_activity_playground/core/enrichment.py +11 -4
- geo_activity_playground/core/missing_values.py +13 -0
- geo_activity_playground/core/test_missing_values.py +19 -0
- geo_activity_playground/explorer/tile_visits.py +1 -1
- geo_activity_playground/webui/app.py +7 -3
- geo_activity_playground/webui/blueprints/activity_blueprint.py +38 -13
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +50 -25
- geo_activity_playground/webui/blueprints/calendar_blueprint.py +12 -4
- geo_activity_playground/webui/blueprints/eddington_blueprints.py +253 -0
- geo_activity_playground/webui/blueprints/entry_views.py +30 -15
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +83 -9
- geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
- geo_activity_playground/webui/blueprints/summary_blueprint.py +102 -42
- geo_activity_playground/webui/columns.py +37 -0
- geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
- geo_activity_playground/webui/templates/activity/show.html.j2 +27 -5
- geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +24 -8
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +150 -0
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +150 -0
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +72 -0
- geo_activity_playground/webui/templates/home.html.j2 +14 -5
- geo_activity_playground/webui/templates/page.html.j2 +10 -1
- geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
- geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
- geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
- geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +91 -2
- {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/RECORD +37 -27
- {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/WHEEL +1 -1
- geo_activity_playground/webui/blueprints/eddington_blueprint.py +0 -194
- /geo_activity_playground/webui/templates/eddington/{index.html.j2 → distance.html.j2} +0 -0
- {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -21,14 +21,15 @@ from PIL import Image
|
|
21
21
|
from PIL import ImageDraw
|
22
22
|
|
23
23
|
from ...core.activities import ActivityRepository
|
24
|
+
from ...core.activities import make_color_bar
|
24
25
|
from ...core.activities import make_geojson_color_line
|
25
26
|
from ...core.activities import make_geojson_from_time_series
|
26
|
-
from ...core.activities import make_speed_color_bar
|
27
27
|
from ...core.config import Config
|
28
28
|
from ...core.datamodel import Activity
|
29
29
|
from ...core.datamodel import DB
|
30
30
|
from ...core.datamodel import Equipment
|
31
31
|
from ...core.datamodel import Kind
|
32
|
+
from ...core.datamodel import Tag
|
32
33
|
from ...core.enrichment import update_via_time_series
|
33
34
|
from ...core.heart_rate import HeartRateZoneComputer
|
34
35
|
from ...core.privacy_zones import PrivacyZone
|
@@ -41,6 +42,8 @@ from ...explorer.grid_file import make_grid_points
|
|
41
42
|
from ...explorer.tile_visits import TileVisitAccessor
|
42
43
|
from ..authenticator import Authenticator
|
43
44
|
from ..authenticator import needs_authentication
|
45
|
+
from ..columns import column_elevation
|
46
|
+
from ..columns import column_speed
|
44
47
|
|
45
48
|
logger = logging.getLogger(__name__)
|
46
49
|
|
@@ -69,14 +72,14 @@ def make_activity_blueprint(
|
|
69
72
|
)
|
70
73
|
]
|
71
74
|
for _, group in repository.get_time_series(
|
72
|
-
activity
|
75
|
+
activity.id
|
73
76
|
).groupby("segment_id")
|
74
77
|
]
|
75
78
|
),
|
76
79
|
properties={
|
77
80
|
"color": matplotlib.colors.to_hex(cmap(i % 8)),
|
78
|
-
"activity_name": activity
|
79
|
-
"activity_id": str(activity
|
81
|
+
"activity_name": activity.name,
|
82
|
+
"activity_id": str(activity.id),
|
80
83
|
},
|
81
84
|
)
|
82
85
|
for i, activity in enumerate(repository.iter_activities())
|
@@ -97,7 +100,7 @@ def make_activity_blueprint(
|
|
97
100
|
|
98
101
|
meta = repository.meta
|
99
102
|
similar_activities = meta.loc[
|
100
|
-
(meta.name == activity
|
103
|
+
(meta.name == activity.name) & (meta.id != activity.id)
|
101
104
|
]
|
102
105
|
similar_activities = [row for _, row in similar_activities.iterrows()]
|
103
106
|
similar_activities.reverse()
|
@@ -105,7 +108,7 @@ def make_activity_blueprint(
|
|
105
108
|
new_tiles = {
|
106
109
|
zoom: sum(
|
107
110
|
tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
|
108
|
-
== activity
|
111
|
+
== activity.id
|
109
112
|
)
|
110
113
|
for zoom in sorted(config.explorer_zoom_levels)
|
111
114
|
}
|
@@ -115,7 +118,7 @@ def make_activity_blueprint(
|
|
115
118
|
for zoom in sorted(config.explorer_zoom_levels):
|
116
119
|
new_tiles = tile_visit_accessor.tile_state["tile_history"][zoom].loc[
|
117
120
|
tile_visit_accessor.tile_state["tile_history"][zoom]["activity_id"]
|
118
|
-
== activity
|
121
|
+
== activity.id
|
119
122
|
]
|
120
123
|
if len(new_tiles):
|
121
124
|
points = make_grid_points(
|
@@ -128,19 +131,34 @@ def make_activity_blueprint(
|
|
128
131
|
new_tiles_geojson[zoom] = make_grid_file_geojson(points)
|
129
132
|
new_tiles_per_zoom[zoom] = len(new_tiles)
|
130
133
|
|
134
|
+
line_color_columns_avail = dict(
|
135
|
+
[(column.name, column) for column in [column_speed, column_elevation]]
|
136
|
+
)
|
137
|
+
line_color_column = (
|
138
|
+
request.args.get("line_color_column")
|
139
|
+
or next(iter(line_color_columns_avail.values())).name
|
140
|
+
)
|
141
|
+
|
131
142
|
context = {
|
132
143
|
"activity": activity,
|
133
144
|
"line_json": line_json,
|
134
145
|
"distance_time_plot": distance_time_plot(time_series),
|
135
|
-
"color_line_geojson": make_geojson_color_line(
|
146
|
+
"color_line_geojson": make_geojson_color_line(
|
147
|
+
time_series, line_color_column
|
148
|
+
),
|
136
149
|
"speed_time_plot": speed_time_plot(time_series),
|
137
150
|
"speed_distribution_plot": speed_distribution_plot(time_series),
|
138
151
|
"similar_activites": similar_activities,
|
139
|
-
"
|
140
|
-
|
141
|
-
|
152
|
+
"line_color_bar": make_color_bar(
|
153
|
+
time_series[line_color_column],
|
154
|
+
line_color_columns_avail[line_color_column].format,
|
155
|
+
),
|
156
|
+
"date": activity.start.date(),
|
157
|
+
"time": activity.start.time(),
|
142
158
|
"new_tiles": new_tiles_per_zoom,
|
143
159
|
"new_tiles_geojson": new_tiles_geojson,
|
160
|
+
"line_color_column": line_color_column,
|
161
|
+
"line_color_columns_avail": line_color_columns_avail,
|
144
162
|
}
|
145
163
|
if (
|
146
164
|
heart_zones := _extract_heart_rate_zones(
|
@@ -301,6 +319,7 @@ def make_activity_blueprint(
|
|
301
319
|
abort(404)
|
302
320
|
equipments = DB.session.scalars(sqlalchemy.select(Equipment)).all()
|
303
321
|
kinds = DB.session.scalars(sqlalchemy.select(Kind)).all()
|
322
|
+
tags = DB.session.scalars(sqlalchemy.select(Tag)).all()
|
304
323
|
|
305
324
|
if request.method == "POST":
|
306
325
|
activity.name = request.form.get("name")
|
@@ -309,13 +328,18 @@ def make_activity_blueprint(
|
|
309
328
|
if form_equipment == "null":
|
310
329
|
activity.equipment = None
|
311
330
|
else:
|
312
|
-
activity.equipment = DB.session.
|
331
|
+
activity.equipment = DB.session.get_one(Equipment, int(form_equipment))
|
313
332
|
|
314
333
|
form_kind = request.form.get("kind")
|
315
334
|
if form_kind == "null":
|
316
335
|
activity.kind = None
|
317
336
|
else:
|
318
|
-
activity.kind = DB.session.
|
337
|
+
activity.kind = DB.session.get_one(Kind, int(form_kind))
|
338
|
+
|
339
|
+
form_tags = request.form.getlist("tag")
|
340
|
+
activity.tags = [
|
341
|
+
DB.session.get_one(Tag, int(tag_id_str)) for tag_id_str in form_tags
|
342
|
+
]
|
319
343
|
|
320
344
|
DB.session.commit()
|
321
345
|
return redirect(url_for(".show", id=activity.id))
|
@@ -325,6 +349,7 @@ def make_activity_blueprint(
|
|
325
349
|
activity=activity,
|
326
350
|
kinds=kinds,
|
327
351
|
equipments=equipments,
|
352
|
+
tags=tags,
|
328
353
|
)
|
329
354
|
|
330
355
|
@blueprint.route("/trim/<id>", methods=["GET", "POST"])
|
@@ -3,6 +3,10 @@ import pandas as pd
|
|
3
3
|
from flask import Blueprint
|
4
4
|
from flask import render_template
|
5
5
|
|
6
|
+
from ..columns import column_distance
|
7
|
+
from ..columns import column_elevation_gain
|
8
|
+
from ..columns import ColumnDescription
|
9
|
+
|
6
10
|
|
7
11
|
def make_bubble_chart_blueprint(repository) -> Blueprint:
|
8
12
|
blueprint = Blueprint("bubble_chart", __name__, template_folder="templates")
|
@@ -19,11 +23,16 @@ def make_bubble_chart_blueprint(repository) -> Blueprint:
|
|
19
23
|
|
20
24
|
# Prepare the bubble chart data
|
21
25
|
bubble_data = activities[
|
22
|
-
[
|
26
|
+
[
|
27
|
+
"start",
|
28
|
+
"kind",
|
29
|
+
"activity_id",
|
30
|
+
column_distance.name,
|
31
|
+
column_elevation_gain.name,
|
32
|
+
]
|
23
33
|
].rename(
|
24
34
|
columns={
|
25
35
|
"start": "date",
|
26
|
-
"distance_km": "distance",
|
27
36
|
"kind": "activity",
|
28
37
|
"activity_id": "id",
|
29
38
|
}
|
@@ -33,29 +42,45 @@ def make_bubble_chart_blueprint(repository) -> Blueprint:
|
|
33
42
|
lambda x: f"/activity/{x}"
|
34
43
|
)
|
35
44
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
y=alt.Y("distance:Q", title="Distance (km)"),
|
43
|
-
size=alt.Size(
|
44
|
-
"distance:Q", scale=alt.Scale(range=[10, 300]), title="Distance"
|
45
|
-
),
|
46
|
-
color=alt.Color("activity:N", title="Activity"),
|
47
|
-
tooltip=[
|
48
|
-
alt.Tooltip("date:T", title="Date"),
|
49
|
-
alt.Tooltip("distance:Q", title="Distance (km)", format=".1f"),
|
50
|
-
alt.Tooltip("activity:N", title="Activity"),
|
51
|
-
alt.Tooltip("activity_url:N", title="Activity Link"),
|
52
|
-
],
|
53
|
-
)
|
54
|
-
.properties(height=800, width=1200)
|
55
|
-
.interactive()
|
56
|
-
.to_json(format="vega")
|
45
|
+
return render_template(
|
46
|
+
"bubble_chart/index.html.j2",
|
47
|
+
bubble_chart_distance=_make_bubble_chart(bubble_data, column_distance),
|
48
|
+
bubble_chart_elevation_gain=_make_bubble_chart(
|
49
|
+
bubble_data, column_elevation_gain
|
50
|
+
),
|
57
51
|
)
|
58
52
|
|
59
|
-
return render_template("bubble_chart/index.html.j2", bubble_chart=bubble_chart)
|
60
|
-
|
61
53
|
return blueprint
|
54
|
+
|
55
|
+
|
56
|
+
def _make_bubble_chart(bubble_data, column: ColumnDescription):
|
57
|
+
return (
|
58
|
+
alt.Chart(bubble_data, title=f"{column.display_name} per Day (Bubble Chart)")
|
59
|
+
.mark_circle()
|
60
|
+
.encode(
|
61
|
+
x=alt.X("date:T", title="Date"),
|
62
|
+
y=alt.Y(
|
63
|
+
f"{column.name}:Q",
|
64
|
+
title=f"{column.display_name} ({column.unit})",
|
65
|
+
),
|
66
|
+
size=alt.Size(
|
67
|
+
f"{column.name}:Q",
|
68
|
+
scale=alt.Scale(range=[10, 300]),
|
69
|
+
title=f"{column.display_name}",
|
70
|
+
),
|
71
|
+
color=alt.Color("activity:N", title="Activity"),
|
72
|
+
tooltip=[
|
73
|
+
alt.Tooltip("date:T", title="Date"),
|
74
|
+
alt.Tooltip(
|
75
|
+
f"{column.name}:Q",
|
76
|
+
title=f"{column.display_name} ({column.unit})",
|
77
|
+
format=column.format,
|
78
|
+
),
|
79
|
+
alt.Tooltip("activity:N", title="Activity"),
|
80
|
+
alt.Tooltip("activity_url:N", title="Activity Link"),
|
81
|
+
],
|
82
|
+
)
|
83
|
+
.properties(height=800, width=1200)
|
84
|
+
.interactive()
|
85
|
+
.to_json(format="vega")
|
86
|
+
)
|
@@ -1,10 +1,14 @@
|
|
1
1
|
import collections
|
2
2
|
import datetime
|
3
3
|
|
4
|
+
import pandas as pd
|
5
|
+
import sqlalchemy
|
4
6
|
from flask import Blueprint
|
5
7
|
from flask import render_template
|
6
8
|
|
7
9
|
from ...core.activities import ActivityRepository
|
10
|
+
from ...core.datamodel import Activity
|
11
|
+
from ...core.datamodel import DB
|
8
12
|
|
9
13
|
|
10
14
|
def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
|
@@ -12,9 +16,14 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
|
|
12
16
|
|
13
17
|
@blueprint.route("/")
|
14
18
|
def index():
|
15
|
-
|
19
|
+
data = DB.session.execute(
|
20
|
+
sqlalchemy.select(Activity.start, Activity.distance_km)
|
21
|
+
).all()
|
22
|
+
df = pd.DataFrame(data)
|
23
|
+
df["year"] = df["start"].dt.year
|
24
|
+
df["month"] = df["start"].dt.month
|
16
25
|
|
17
|
-
monthly_distance =
|
26
|
+
monthly_distance = df.groupby(
|
18
27
|
["year", "month"],
|
19
28
|
).apply(lambda group: sum(group["distance_km"]), include_groups=False)
|
20
29
|
monthly_distance.name = "total_distance_km"
|
@@ -24,7 +33,7 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
|
|
24
33
|
.fillna(0.0)
|
25
34
|
)
|
26
35
|
|
27
|
-
yearly_distance =
|
36
|
+
yearly_distance = df.groupby(["year"]).apply(
|
28
37
|
lambda group: sum(group["distance_km"]), include_groups=False
|
29
38
|
)
|
30
39
|
yearly_distance.name = "total_distance_km"
|
@@ -34,7 +43,6 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
|
|
34
43
|
}
|
35
44
|
|
36
45
|
context = {
|
37
|
-
"num_activities": len(repository),
|
38
46
|
"monthly_distances": monthly_pivot,
|
39
47
|
"yearly_distances": yearly_distances,
|
40
48
|
}
|
@@ -0,0 +1,253 @@
|
|
1
|
+
import datetime
|
2
|
+
from math import ceil
|
3
|
+
|
4
|
+
import altair as alt
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
from flask import Blueprint
|
8
|
+
from flask import render_template
|
9
|
+
from flask import Request
|
10
|
+
from flask import request
|
11
|
+
|
12
|
+
from ...core.activities import ActivityRepository
|
13
|
+
from ...core.meta_search import apply_search_query
|
14
|
+
from ..columns import column_distance
|
15
|
+
from ..columns import column_elevation_gain
|
16
|
+
from ..columns import ColumnDescription
|
17
|
+
from ..search_util import search_query_from_form
|
18
|
+
from ..search_util import SearchQueryHistory
|
19
|
+
|
20
|
+
|
21
|
+
def register_eddington_blueprint(
|
22
|
+
repository: ActivityRepository, search_query_history: SearchQueryHistory
|
23
|
+
) -> Blueprint:
|
24
|
+
blueprint = Blueprint("eddington", __name__, template_folder="templates")
|
25
|
+
|
26
|
+
@blueprint.route("/")
|
27
|
+
def distance():
|
28
|
+
return _render_eddington_template(
|
29
|
+
repository, request, search_query_history, "distance", column_distance, [1]
|
30
|
+
)
|
31
|
+
|
32
|
+
@blueprint.route("/elevation_gain")
|
33
|
+
def elevation_gain():
|
34
|
+
return _render_eddington_template(
|
35
|
+
repository,
|
36
|
+
request,
|
37
|
+
search_query_history,
|
38
|
+
"elevation_gain",
|
39
|
+
column_elevation_gain,
|
40
|
+
[20, 10, 1],
|
41
|
+
)
|
42
|
+
|
43
|
+
return blueprint
|
44
|
+
|
45
|
+
|
46
|
+
def _render_eddington_template(
|
47
|
+
repository: ActivityRepository,
|
48
|
+
request: Request,
|
49
|
+
search_query_history: SearchQueryHistory,
|
50
|
+
template_name,
|
51
|
+
column: ColumnDescription,
|
52
|
+
divisor_values_avail: list[int],
|
53
|
+
) -> str:
|
54
|
+
|
55
|
+
column_name = column.name
|
56
|
+
display_name = column.display_name
|
57
|
+
divisor = int(request.args.get("eddington_divisor") or divisor_values_avail[0])
|
58
|
+
|
59
|
+
query = search_query_from_form(request.args)
|
60
|
+
search_query_history.register_query(query)
|
61
|
+
activities = (
|
62
|
+
apply_search_query(repository.meta, query)
|
63
|
+
.dropna(subset=["start", column_name])
|
64
|
+
.copy()
|
65
|
+
)
|
66
|
+
|
67
|
+
activities["year"] = [start.year for start in activities["start"]]
|
68
|
+
activities["date"] = [start.date() for start in activities["start"]]
|
69
|
+
activities["isoyear"] = [start.isocalendar().year for start in activities["start"]]
|
70
|
+
activities["isoweek"] = [start.isocalendar().week for start in activities["start"]]
|
71
|
+
|
72
|
+
en_per_day, eddington_df_per_day = _get_values_per_group(
|
73
|
+
activities.groupby("date"), column_name, divisor
|
74
|
+
)
|
75
|
+
en_per_week, eddington_df_per_week = _get_values_per_group(
|
76
|
+
activities.groupby(["isoyear", "isoweek"]), column_name, divisor
|
77
|
+
)
|
78
|
+
|
79
|
+
return render_template(
|
80
|
+
f"eddington/{template_name}.html.j2",
|
81
|
+
eddington_number=en_per_day,
|
82
|
+
logarithmic_plot=_make_eddington_plot(
|
83
|
+
eddington_df_per_day, en_per_day, "Days", column_name, display_name, divisor
|
84
|
+
),
|
85
|
+
eddington_per_week=en_per_week,
|
86
|
+
eddington_per_week_plot=_make_eddington_plot(
|
87
|
+
eddington_df_per_week,
|
88
|
+
en_per_week,
|
89
|
+
"Weeks",
|
90
|
+
column_name,
|
91
|
+
display_name,
|
92
|
+
divisor,
|
93
|
+
),
|
94
|
+
eddington_table=eddington_df_per_day.loc[
|
95
|
+
(eddington_df_per_day[column_name] > en_per_day)
|
96
|
+
& (eddington_df_per_day[column_name] <= en_per_day + 10 * divisor)
|
97
|
+
& (eddington_df_per_day[column_name] % divisor == 0)
|
98
|
+
].to_dict(orient="records"),
|
99
|
+
eddington_table_weeks=eddington_df_per_week.loc[
|
100
|
+
(eddington_df_per_week[column_name] > en_per_week)
|
101
|
+
& (eddington_df_per_week[column_name] <= en_per_week + 10 * divisor)
|
102
|
+
& (eddington_df_per_week[column_name] % divisor == 0)
|
103
|
+
].to_dict(orient="records"),
|
104
|
+
query=query.to_jinja(),
|
105
|
+
yearly_eddington=_get_yearly_eddington(activities, column_name, divisor),
|
106
|
+
eddington_number_history_plot=_get_eddington_number_history(
|
107
|
+
activities, column_name, divisor
|
108
|
+
),
|
109
|
+
eddington_divisor=divisor,
|
110
|
+
divisor_values_avail=divisor_values_avail,
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
def _get_values_per_group(grouped, columnName, divisor) -> tuple[int, pd.DataFrame]:
|
115
|
+
sum_per_group = grouped.apply(
|
116
|
+
lambda group: int(sum(group[columnName])), include_groups=False
|
117
|
+
)
|
118
|
+
counts = dict(zip(*np.unique(sorted(sum_per_group), return_counts=True)))
|
119
|
+
eddington = pd.DataFrame(
|
120
|
+
{columnName: d, "count": counts.get(d, 0)}
|
121
|
+
for d in range(max(counts.keys()) + 1)
|
122
|
+
)
|
123
|
+
eddington["total"] = eddington["count"][::-1].cumsum()[::-1]
|
124
|
+
eddington[f"{columnName}_div"] = eddington[columnName] // divisor
|
125
|
+
en = (
|
126
|
+
eddington.loc[eddington["total"] >= eddington[f"{columnName}_div"]][
|
127
|
+
"total"
|
128
|
+
].iloc[-1]
|
129
|
+
* divisor
|
130
|
+
)
|
131
|
+
eddington["missing"] = eddington[f"{columnName}_div"] - eddington["total"]
|
132
|
+
|
133
|
+
return en, eddington
|
134
|
+
|
135
|
+
|
136
|
+
def _make_eddington_plot(
|
137
|
+
eddington_df: pd.DataFrame,
|
138
|
+
en: int,
|
139
|
+
interval: str,
|
140
|
+
column_name: str,
|
141
|
+
display_name: str,
|
142
|
+
divisor: int,
|
143
|
+
) -> dict:
|
144
|
+
x = list(range(1, max(eddington_df[column_name]) + 1))
|
145
|
+
y = [v / divisor for v in x]
|
146
|
+
return (
|
147
|
+
(
|
148
|
+
(
|
149
|
+
alt.Chart(
|
150
|
+
eddington_df,
|
151
|
+
height=500,
|
152
|
+
width=800,
|
153
|
+
title=f"{display_name} Eddington Number {en}",
|
154
|
+
)
|
155
|
+
.mark_area(interpolate="step")
|
156
|
+
.encode(
|
157
|
+
alt.X(
|
158
|
+
column_name,
|
159
|
+
scale=alt.Scale(domainMin=0, domainMax=en * 3),
|
160
|
+
title=display_name,
|
161
|
+
),
|
162
|
+
alt.Y(
|
163
|
+
"total",
|
164
|
+
scale=alt.Scale(domainMax=en / divisor * 1.5),
|
165
|
+
title=f"{interval} exceeding {display_name}",
|
166
|
+
),
|
167
|
+
[
|
168
|
+
alt.Tooltip(column_name, title=display_name),
|
169
|
+
alt.Tooltip(
|
170
|
+
"total", title=f"{interval} exceeding {display_name}"
|
171
|
+
),
|
172
|
+
alt.Tooltip("missing", title=f"{interval} missing for next"),
|
173
|
+
],
|
174
|
+
)
|
175
|
+
)
|
176
|
+
+ (
|
177
|
+
alt.Chart(pd.DataFrame({column_name: x, "total": y}))
|
178
|
+
.mark_line(color="red")
|
179
|
+
.encode(alt.X(column_name), alt.Y("total"))
|
180
|
+
)
|
181
|
+
)
|
182
|
+
.interactive(bind_x=True, bind_y=True)
|
183
|
+
.to_json(format="vega")
|
184
|
+
)
|
185
|
+
|
186
|
+
|
187
|
+
def _get_eddington_number(elevation_gains: pd.Series, divisor: int) -> int:
|
188
|
+
if len(elevation_gains) == 1:
|
189
|
+
if elevation_gains.iloc[0] >= 1:
|
190
|
+
return 1
|
191
|
+
else:
|
192
|
+
0
|
193
|
+
|
194
|
+
sorted_elevation_gains = sorted(elevation_gains, reverse=True)
|
195
|
+
|
196
|
+
for number_of_days, elevation_gain in enumerate(sorted_elevation_gains, 1):
|
197
|
+
if elevation_gain / divisor < number_of_days:
|
198
|
+
return (number_of_days - 1) * divisor
|
199
|
+
|
200
|
+
|
201
|
+
def _get_yearly_eddington(
|
202
|
+
meta: pd.DataFrame, columnName: str, divisor: int
|
203
|
+
) -> dict[int, int]:
|
204
|
+
meta = meta.dropna(subset=["start", columnName]).copy()
|
205
|
+
meta["year"] = [start.year for start in meta["start"]]
|
206
|
+
meta["date"] = [start.date() for start in meta["start"]]
|
207
|
+
|
208
|
+
yearly_eddington = meta.groupby("year").apply(
|
209
|
+
lambda group: _get_eddington_number(
|
210
|
+
group.groupby("date").apply(
|
211
|
+
lambda group2: int(group2[columnName].sum()), include_groups=False
|
212
|
+
),
|
213
|
+
divisor,
|
214
|
+
),
|
215
|
+
include_groups=False,
|
216
|
+
)
|
217
|
+
return yearly_eddington.to_dict()
|
218
|
+
|
219
|
+
|
220
|
+
def _get_eddington_number_history(
|
221
|
+
meta: pd.DataFrame, columnName: str, divisor: int
|
222
|
+
) -> dict:
|
223
|
+
|
224
|
+
daily_elevation_gains = meta.groupby("date").apply(
|
225
|
+
lambda group2: int(group2[columnName].sum()), include_groups=False
|
226
|
+
)
|
227
|
+
|
228
|
+
eddington_number_history = {"date": [], "eddington_number": []}
|
229
|
+
top_days = []
|
230
|
+
for date, elevation_gain in daily_elevation_gains.items():
|
231
|
+
elevation_gain = elevation_gain / divisor
|
232
|
+
if len(top_days) == 0:
|
233
|
+
top_days.append(elevation_gain)
|
234
|
+
else:
|
235
|
+
if elevation_gain >= top_days[0]:
|
236
|
+
top_days.append(elevation_gain)
|
237
|
+
top_days.sort()
|
238
|
+
while top_days[0] < len(top_days):
|
239
|
+
top_days.pop(0)
|
240
|
+
eddington_number_history["date"].append(
|
241
|
+
datetime.datetime.combine(date, datetime.datetime.min.time())
|
242
|
+
)
|
243
|
+
eddington_number_history["eddington_number"].append(len(top_days) * divisor)
|
244
|
+
history = pd.DataFrame(eddington_number_history)
|
245
|
+
|
246
|
+
return (
|
247
|
+
alt.Chart(history)
|
248
|
+
.mark_line(interpolate="step-after")
|
249
|
+
.encode(
|
250
|
+
alt.X("date", title="Date"),
|
251
|
+
alt.Y("eddington_number", title="Eddington number"),
|
252
|
+
)
|
253
|
+
).to_json(format="vega")
|
@@ -4,12 +4,18 @@ import datetime
|
|
4
4
|
import altair as alt
|
5
5
|
import flask
|
6
6
|
import pandas as pd
|
7
|
+
import sqlalchemy
|
7
8
|
from flask import render_template
|
8
9
|
from flask import Response
|
9
10
|
|
10
11
|
from ...core.activities import ActivityRepository
|
11
12
|
from ...core.activities import make_geojson_from_time_series
|
12
13
|
from ...core.config import Config
|
14
|
+
from ...core.datamodel import Activity
|
15
|
+
from ...core.datamodel import DB
|
16
|
+
from ..columns import column_distance
|
17
|
+
from ..columns import column_elevation_gain
|
18
|
+
from ..columns import ColumnDescription
|
13
19
|
from ..plot_util import make_kind_scale
|
14
20
|
|
15
21
|
|
@@ -22,27 +28,32 @@ def register_entry_views(
|
|
22
28
|
|
23
29
|
if len(repository):
|
24
30
|
kind_scale = make_kind_scale(repository.meta, config)
|
25
|
-
context["distance_last_30_days_plot"] =
|
26
|
-
repository.meta, kind_scale
|
31
|
+
context["distance_last_30_days_plot"] = _last_30_days_meta_plot(
|
32
|
+
repository.meta, kind_scale, column_distance
|
33
|
+
)
|
34
|
+
context["elevation_gain_last_30_days_plot"] = _last_30_days_meta_plot(
|
35
|
+
repository.meta, kind_scale, column_elevation_gain
|
27
36
|
)
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
for index, activity in activity_meta.iterrows():
|
35
|
-
time_series = repository.get_time_series(activity["id"])
|
36
|
-
context["latest_activities"][date].append(
|
38
|
+
context["latest_activities"] = collections.defaultdict(list)
|
39
|
+
for activity in DB.session.scalars(
|
40
|
+
sqlalchemy.select(Activity).order_by(Activity.start.desc()).limit(100)
|
41
|
+
):
|
42
|
+
context["latest_activities"][activity.start.date()].append(
|
37
43
|
{
|
38
44
|
"activity": activity,
|
39
|
-
"line_geojson": make_geojson_from_time_series(
|
45
|
+
"line_geojson": make_geojson_from_time_series(
|
46
|
+
activity.time_series
|
47
|
+
),
|
40
48
|
}
|
41
49
|
)
|
50
|
+
|
42
51
|
return render_template("home.html.j2", **context)
|
43
52
|
|
44
53
|
|
45
|
-
def
|
54
|
+
def _last_30_days_meta_plot(
|
55
|
+
meta: pd.DataFrame, kind_scale: alt.Scale, column: ColumnDescription
|
56
|
+
) -> str:
|
46
57
|
before_30_days = pd.to_datetime(
|
47
58
|
datetime.datetime.now() - datetime.timedelta(days=31)
|
48
59
|
)
|
@@ -51,17 +62,21 @@ def _distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale)
|
|
51
62
|
meta.loc[meta["start"] > before_30_days],
|
52
63
|
width=700,
|
53
64
|
height=200,
|
54
|
-
title="
|
65
|
+
title=f"{column.display_name} per day",
|
55
66
|
)
|
56
67
|
.mark_bar()
|
57
68
|
.encode(
|
58
69
|
alt.X("yearmonthdate(start)", title="Date"),
|
59
|
-
alt.Y("sum(
|
70
|
+
alt.Y(f"sum({column.name})", title=f"{column.name} / {column.unit}"),
|
60
71
|
alt.Color("kind", scale=kind_scale, title="Kind"),
|
61
72
|
[
|
62
73
|
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
63
74
|
alt.Tooltip("kind", title="Kind"),
|
64
|
-
alt.Tooltip(
|
75
|
+
alt.Tooltip(
|
76
|
+
f"sum({column.name})",
|
77
|
+
format=column.format,
|
78
|
+
title=f"{column.display_name} / {column.unit}",
|
79
|
+
),
|
65
80
|
],
|
66
81
|
)
|
67
82
|
.to_json(format="vega")
|