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.
Files changed (38) hide show
  1. geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +70 -0
  2. geo_activity_playground/alembic/versions/script.py.mako +0 -6
  3. geo_activity_playground/core/activities.py +21 -44
  4. geo_activity_playground/core/datamodel.py +121 -60
  5. geo_activity_playground/core/enrichment.py +11 -4
  6. geo_activity_playground/core/missing_values.py +13 -0
  7. geo_activity_playground/core/test_missing_values.py +19 -0
  8. geo_activity_playground/explorer/tile_visits.py +1 -1
  9. geo_activity_playground/webui/app.py +7 -3
  10. geo_activity_playground/webui/blueprints/activity_blueprint.py +38 -13
  11. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +50 -25
  12. geo_activity_playground/webui/blueprints/calendar_blueprint.py +12 -4
  13. geo_activity_playground/webui/blueprints/eddington_blueprints.py +253 -0
  14. geo_activity_playground/webui/blueprints/entry_views.py +30 -15
  15. geo_activity_playground/webui/blueprints/explorer_blueprint.py +83 -9
  16. geo_activity_playground/webui/blueprints/settings_blueprint.py +32 -0
  17. geo_activity_playground/webui/blueprints/summary_blueprint.py +102 -42
  18. geo_activity_playground/webui/columns.py +37 -0
  19. geo_activity_playground/webui/templates/activity/edit.html.j2 +15 -0
  20. geo_activity_playground/webui/templates/activity/show.html.j2 +27 -5
  21. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +24 -8
  22. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +150 -0
  23. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +150 -0
  24. geo_activity_playground/webui/templates/explorer/server-side.html.j2 +72 -0
  25. geo_activity_playground/webui/templates/home.html.j2 +14 -5
  26. geo_activity_playground/webui/templates/page.html.j2 +10 -1
  27. geo_activity_playground/webui/templates/settings/index.html.j2 +9 -0
  28. geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +17 -0
  29. geo_activity_playground/webui/templates/settings/tags-list.html.j2 +19 -0
  30. geo_activity_playground/webui/templates/settings/tags-new.html.j2 +17 -0
  31. geo_activity_playground/webui/templates/summary/index.html.j2 +91 -2
  32. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/METADATA +2 -1
  33. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/RECORD +37 -27
  34. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/WHEEL +1 -1
  35. geo_activity_playground/webui/blueprints/eddington_blueprint.py +0 -194
  36. /geo_activity_playground/webui/templates/eddington/{index.html.j2 → distance.html.j2} +0 -0
  37. {geo_activity_playground-0.40.1.dist-info → geo_activity_playground-0.42.0.dist-info}/LICENSE +0 -0
  38. {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["id"]
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["name"],
79
- "activity_id": str(activity["id"]),
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["name"]) & (meta.id != activity["id"])
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["id"]
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["id"]
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(time_series),
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
- "speed_color_bar": make_speed_color_bar(time_series),
140
- "date": activity["start"].date(),
141
- "time": activity["start"].time(),
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.get(Equipment, int(form_equipment))
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.get(Kind, int(form_kind))
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
- ["start", "distance_km", "kind", "activity_id"]
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
- # Create the bubble chart
37
- bubble_chart = (
38
- alt.Chart(bubble_data, title="Distance per Day (Bubble Chart)")
39
- .mark_circle()
40
- .encode(
41
- x=alt.X("date:T", title="Date"),
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
- meta = repository.meta
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 = meta.groupby(
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 = meta.groupby(["year"]).apply(
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"] = _distance_last_30_days_meta_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
- meta = repository.meta.copy()
30
- meta["date"] = meta["start"].dt.date
31
-
32
- context["latest_activities"] = collections.defaultdict(list)
33
- for date, activity_meta in list(meta.groupby("date"))[:-30:-1]:
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(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 _distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
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="Distance per day",
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(distance_km)", title="Distance / km"),
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("sum(distance_km)", format=".1f", title="Distance / km"),
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")