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
@@ -1,12 +1,15 @@
1
1
  import datetime
2
+ import io
2
3
  import itertools
3
4
  import logging
4
5
 
5
6
  import altair as alt
6
7
  import geojson
7
8
  import matplotlib
9
+ import matplotlib.pyplot as pl
8
10
  import numpy as np
9
11
  import pandas as pd
12
+ import sqlalchemy
10
13
  from flask import Blueprint
11
14
  from flask import flash
12
15
  from flask import redirect
@@ -17,6 +20,10 @@ from flask import url_for
17
20
  from ...core.activities import ActivityRepository
18
21
  from ...core.config import ConfigAccessor
19
22
  from ...core.coordinates import Bounds
23
+ from ...core.datamodel import Activity
24
+ from ...core.datamodel import DB
25
+ from ...core.raster_map import ImageTransform
26
+ from ...core.raster_map import TileGetter
20
27
  from ...core.tiles import compute_tile
21
28
  from ...core.tiles import get_tile_upper_left_lat_lon
22
29
  from ...explorer.grid_file import get_border_tiles
@@ -38,9 +45,10 @@ logger = logging.getLogger(__name__)
38
45
 
39
46
  def make_explorer_blueprint(
40
47
  authenticator: Authenticator,
41
- repository: ActivityRepository,
42
48
  tile_visit_accessor: TileVisitAccessor,
43
49
  config_accessor: ConfigAccessor,
50
+ tile_getter: TileGetter,
51
+ image_transforms: dict[str, ImageTransform],
44
52
  ) -> Blueprint:
45
53
  blueprint = Blueprint("explorer", __name__, template_folder="templates")
46
54
 
@@ -59,7 +67,7 @@ def make_explorer_blueprint(
59
67
  )
60
68
 
61
69
  explored = get_three_color_tiles(
62
- tile_visits[zoom], repository, tile_evolution_states[zoom], zoom
70
+ tile_visits[zoom], tile_evolution_states[zoom], zoom
63
71
  )
64
72
 
65
73
  context = {
@@ -151,12 +159,74 @@ def make_explorer_blueprint(
151
159
  headers={"Content-disposition": "attachment"},
152
160
  )
153
161
 
162
+ @blueprint.route("/<int:zoom>/server-side")
163
+ def server_side(zoom: int):
164
+ if zoom not in config_accessor().explorer_zoom_levels:
165
+ return {"zoom_level_not_generated": zoom}
166
+
167
+ tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
168
+ tile_histories = tile_visit_accessor.tile_state["tile_history"]
169
+
170
+ medians = tile_histories[zoom].median()
171
+ median_lat, median_lon = get_tile_upper_left_lat_lon(
172
+ medians["tile_x"], medians["tile_y"], zoom
173
+ )
174
+
175
+ context = {
176
+ "center": {
177
+ "latitude": median_lat,
178
+ "longitude": median_lon,
179
+ "bbox": (
180
+ bounding_box_for_biggest_cluster(
181
+ tile_evolution_states[zoom].clusters.values(), zoom
182
+ )
183
+ if len(tile_evolution_states[zoom].memberships) > 0
184
+ else {}
185
+ ),
186
+ },
187
+ "plot_tile_evolution": plot_tile_evolution(tile_histories[zoom]),
188
+ "plot_cluster_evolution": plot_cluster_evolution(
189
+ tile_evolution_states[zoom].cluster_evolution
190
+ ),
191
+ "plot_square_evolution": plot_square_evolution(
192
+ tile_evolution_states[zoom].square_evolution
193
+ ),
194
+ "zoom": zoom,
195
+ }
196
+ return render_template("explorer/server-side.html.j2", **context)
197
+
198
+ @blueprint.route("/<int:zoom>/tile/<int:z>/<int:x>/<int:y>.png")
199
+ def tile(zoom: int, z: int, x: int, y: int) -> Response:
200
+ tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
201
+
202
+ map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
203
+ if z >= zoom:
204
+ factor = 2 ** (z - zoom)
205
+ if (x // factor, y // factor) in tile_visits:
206
+ map_tile = image_transforms["color"].transform_image(map_tile)
207
+ else:
208
+ map_tile = image_transforms["color"].transform_image(map_tile) / 1.2
209
+ else:
210
+ grayscale = image_transforms["color"].transform_image(map_tile) / 1.2
211
+ factor = 2 ** (zoom - z)
212
+ width = 256 // factor
213
+ for xo in range(factor):
214
+ for yo in range(factor):
215
+ if (x * factor + xo, y * factor + yo) not in tile_visits:
216
+ map_tile[
217
+ yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
218
+ ] = grayscale[
219
+ yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
220
+ ]
221
+ f = io.BytesIO()
222
+ pl.imsave(f, map_tile, format="png")
223
+ return Response(bytes(f.getbuffer()), mimetype="image/png")
224
+
154
225
  return blueprint
155
226
 
156
227
 
157
228
  def get_three_color_tiles(
158
229
  tile_visits: dict,
159
- repository: ActivityRepository,
160
230
  cluster_state: TileEvolutionState,
161
231
  zoom: int,
162
232
  ) -> str:
@@ -174,13 +244,17 @@ def get_three_color_tiles(
174
244
  last_age_days = 10000
175
245
  tile_dict[tile] = {
176
246
  "first_activity_id": str(tile_data["first_id"]),
177
- "first_activity_name": repository.get_activity_by_id(tile_data["first_id"])[
178
- "name"
179
- ],
247
+ "first_activity_name": DB.session.scalar(
248
+ sqlalchemy.select(Activity.name).where(
249
+ Activity.id == tile_data["first_id"]
250
+ )
251
+ ),
180
252
  "last_activity_id": str(tile_data["last_id"]),
181
- "last_activity_name": repository.get_activity_by_id(tile_data["last_id"])[
182
- "name"
183
- ],
253
+ "last_activity_name": DB.session.scalar(
254
+ sqlalchemy.select(Activity.name).where(
255
+ Activity.id == tile_data["last_id"]
256
+ )
257
+ ),
184
258
  "first_age_days": first_age_days,
185
259
  "first_age_color": matplotlib.colors.to_hex(
186
260
  cmap_first(max(1 - first_age_days / (2 * 365), 0.0))
@@ -18,6 +18,7 @@ from ...core.config import ConfigAccessor
18
18
  from ...core.datamodel import DB
19
19
  from ...core.datamodel import Equipment
20
20
  from ...core.datamodel import Kind
21
+ from ...core.datamodel import Tag
21
22
  from ...core.heart_rate import HeartRateZoneComputer
22
23
  from ...core.paths import _activity_enriched_dir
23
24
  from ..authenticator import Authenticator
@@ -395,6 +396,37 @@ def make_settings_blueprint(
395
396
  strava_login_helper.save_strava_code(code)
396
397
  return redirect(url_for(".strava"))
397
398
 
399
+ @blueprint.route("/tags")
400
+ @needs_authentication(authenticator)
401
+ def tags_list():
402
+ return render_template(
403
+ "settings/tags-list.html.j2",
404
+ tags=DB.session.scalars(sqlalchemy.select(Tag)).all(),
405
+ )
406
+
407
+ @blueprint.route("/tags/new", methods=["GET", "POST"])
408
+ @needs_authentication(authenticator)
409
+ def tags_new():
410
+ if request.method == "POST":
411
+ tag_str = request.form["tag"]
412
+ tag = Tag(tag=tag_str)
413
+ DB.session.add(tag)
414
+ DB.session.commit()
415
+ return redirect(url_for(".tags_list"))
416
+ else:
417
+ return render_template("settings/tags-new.html.j2")
418
+
419
+ @blueprint.route("/tags/edit/<int:id>", methods=["GET", "POST"])
420
+ @needs_authentication(authenticator)
421
+ def tags_edit(id: int):
422
+ tag = DB.session.get_one(Tag, id)
423
+ if request.method == "POST":
424
+ tag.tag = request.form["tag"]
425
+ DB.session.commit()
426
+ return redirect(url_for(".tags_list"))
427
+ else:
428
+ return render_template("settings/tags-edit.html.j2", tag=tag)
429
+
398
430
  return blueprint
399
431
 
400
432
 
@@ -15,6 +15,9 @@ from ...core.datamodel import DB
15
15
  from ...core.datamodel import PlotSpec
16
16
  from ...core.meta_search import apply_search_query
17
17
  from ...core.parametric_plot import make_parametric_plot
18
+ from ..columns import column_distance
19
+ from ..columns import column_elevation_gain
20
+ from ..columns import ColumnDescription
18
21
  from ..plot_util import make_kind_scale
19
22
  from ..search_util import search_query_from_form
20
23
  from ..search_util import SearchQueryHistory
@@ -36,25 +39,38 @@ def make_summary_blueprint(
36
39
  kind_scale = make_kind_scale(repository.meta, config)
37
40
  df = activities
38
41
 
39
- year_kind_total = (
40
- df[["year", "kind", "distance_km", "hours"]]
41
- .groupby(["year", "kind"])
42
- .sum()
43
- .reset_index()
44
- )
45
-
46
42
  nominations = nominate_activities(df)
47
43
 
48
44
  return render_template(
49
45
  "summary/index.html.j2",
50
- plot_distance_heatmaps=plot_distance_heatmaps(df, config),
51
- plot_monthly_distance=plot_monthly_distance(df, kind_scale),
52
- plot_yearly_distance=plot_yearly_distance(year_kind_total, kind_scale),
53
- plot_year_cumulative=plot_year_cumulative(df),
54
- tabulate_year_kind_mean=tabulate_year_kind_mean(df)
46
+ plot_distance_heatmaps=plot_heatmaps(df, column_distance, config),
47
+ plot_elevation_gain_heatmaps=plot_heatmaps(
48
+ df, column_elevation_gain, config
49
+ ),
50
+ plot_monthly_distance=plot_monthly_sums(df, column_distance, kind_scale),
51
+ plot_monthly_elevation_gain=plot_monthly_sums(
52
+ df, column_elevation_gain, kind_scale
53
+ ),
54
+ plot_yearly_distance=plot_yearly_sums(df, column_distance, kind_scale),
55
+ plot_yearly_elevation_gain=plot_yearly_sums(
56
+ df, column_elevation_gain, kind_scale
57
+ ),
58
+ plot_year_cumulative=plot_year_cumulative(df, column_distance),
59
+ plot_year_elevation_gain_cumulative=plot_year_cumulative(
60
+ df, column_elevation_gain
61
+ ),
62
+ tabulate_year_kind_mean=tabulate_year_kind_mean(df, column_distance)
55
63
  .reset_index()
56
64
  .to_dict(orient="split"),
57
- plot_weekly_distance=plot_weekly_distance(df, kind_scale),
65
+ tabulate_year_kind_mean_elevation_gain=tabulate_year_kind_mean(
66
+ df, column_elevation_gain
67
+ )
68
+ .reset_index()
69
+ .to_dict(orient="split"),
70
+ plot_weekly_distance=plot_weekly_sums(df, column_distance, kind_scale),
71
+ plot_weekly_elevation_gain=plot_weekly_sums(
72
+ df, column_elevation_gain, kind_scale
73
+ ),
58
74
  nominations=[
59
75
  (
60
76
  repository.get_activity_by_id(activity_id),
@@ -109,11 +125,13 @@ def _nominate_activities_inner(
109
125
  nominations[i].append(f"{title}{title_suffix}: {format_applied}")
110
126
 
111
127
 
112
- def plot_distance_heatmaps(meta: pd.DataFrame, config: Config) -> dict[int, str]:
128
+ def plot_heatmaps(
129
+ meta: pd.DataFrame, column: ColumnDescription, config: Config
130
+ ) -> dict[int, str]:
113
131
  return {
114
132
  year: alt.Chart(
115
133
  meta.loc[(meta["year"] == year)],
116
- title="Daily Distance Heatmap",
134
+ title=f"Daily {column.display_name} Heatmap",
117
135
  )
118
136
  .mark_rect()
119
137
  .encode(
@@ -124,15 +142,17 @@ def plot_distance_heatmaps(meta: pd.DataFrame, config: Config) -> dict[int, str]
124
142
  title="Year and month",
125
143
  ),
126
144
  alt.Color(
127
- "sum(distance_km)",
145
+ f"sum({column.name})",
128
146
  scale=alt.Scale(scheme=config.color_scheme_for_counts),
129
147
  ),
130
148
  [
131
149
  alt.Tooltip("yearmonthdate(start)", title="Date"),
132
150
  alt.Tooltip(
133
- "sum(distance_km)", format=".1f", title="Total distance / km"
151
+ f"sum({column.name})",
152
+ format=column.format,
153
+ title=f"Total {column.display_name} / {column.unit}",
134
154
  ),
135
- alt.Tooltip("count(distance_km)", title="Number of activities"),
155
+ alt.Tooltip(f"count({column.name})", title="Number of activities"),
136
156
  ],
137
157
  )
138
158
  .to_json(format="vega")
@@ -140,7 +160,9 @@ def plot_distance_heatmaps(meta: pd.DataFrame, config: Config) -> dict[int, str]
140
160
  }
141
161
 
142
162
 
143
- def plot_monthly_distance(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
163
+ def plot_monthly_sums(
164
+ meta: pd.DataFrame, column: ColumnDescription, kind_scale: alt.Scale
165
+ ) -> str:
144
166
  return (
145
167
  alt.Chart(
146
168
  meta.loc[
@@ -151,20 +173,26 @@ def plot_monthly_distance(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
151
173
  )
152
174
  )
153
175
  ],
154
- title="Monthly Distance",
176
+ title=f"Monthly {column.display_name}",
155
177
  )
156
178
  .mark_bar()
157
179
  .encode(
158
180
  alt.X("month(start)", title="Month"),
159
- alt.Y("sum(distance_km)", title="Distance / km"),
181
+ alt.Y(
182
+ f"sum({column.name})",
183
+ title=f"{column.display_name} / {column.unit}",
184
+ ),
160
185
  alt.Color("kind", scale=kind_scale, title="Kind"),
161
186
  alt.Column("year(start):O", title="Year"),
162
187
  [
163
- alt.Tooltip("yearmonthdate(start)", title="Date"),
188
+ alt.Tooltip("yearmonth(start)", title="Year and Month"),
189
+ alt.Tooltip("kind", title="Kind"),
164
190
  alt.Tooltip(
165
- "sum(distance_km)", format=".1f", title="Total distance / km"
191
+ f"sum({column.name})",
192
+ format=column.format,
193
+ title=f"Total {column.display_name} / {column.unit}",
166
194
  ),
167
- alt.Tooltip("count(distance_km)", title="Number of activities"),
195
+ alt.Tooltip(f"count({column.name})", title="Number of activities"),
168
196
  ],
169
197
  )
170
198
  .resolve_axis(x="independent")
@@ -172,31 +200,47 @@ def plot_monthly_distance(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
172
200
  )
173
201
 
174
202
 
175
- def plot_yearly_distance(year_kind_total: pd.DataFrame, kind_scale: alt.Scale) -> str:
203
+ def plot_yearly_sums(
204
+ df: pd.DataFrame, column: ColumnDescription, kind_scale: alt.Scale
205
+ ) -> str:
206
+ year_kind_total = (
207
+ df[["year", "kind", column.name, "hours"]]
208
+ .groupby(["year", "kind"])
209
+ .sum()
210
+ .reset_index()
211
+ )
212
+
176
213
  return (
177
- alt.Chart(year_kind_total, title="Total Distance per Year")
214
+ alt.Chart(year_kind_total, title=f"Total {column.display_name} per Year")
178
215
  .mark_bar()
179
216
  .encode(
180
217
  alt.X("year:O", title="Year"),
181
- alt.Y("distance_km", title="Distance / km"),
218
+ alt.Y(column.name, title=f"{column.display_name} / {column.unit}"),
182
219
  alt.Color("kind", scale=kind_scale, title="Kind"),
183
220
  [
184
221
  alt.Tooltip("year:O", title="Year"),
185
222
  alt.Tooltip("kind", title="Kind"),
186
- alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
223
+ alt.Tooltip(
224
+ column.name,
225
+ title=f"{column.display_name} / {column.unit}",
226
+ format=column.format,
227
+ ),
187
228
  ],
188
229
  )
189
230
  .to_json(format="vega")
190
231
  )
191
232
 
192
233
 
193
- def plot_year_cumulative(df: pd.DataFrame) -> str:
234
+ def plot_year_cumulative(df: pd.DataFrame, column: ColumnDescription) -> str:
194
235
  year_cumulative = (
195
- df[["iso_year", "week", "distance_km"]]
236
+ df[["iso_year", "week", column.name]]
196
237
  .groupby("iso_year")
197
238
  .apply(
198
239
  lambda group: pd.DataFrame(
199
- {"week": group["week"], "distance_km": group["distance_km"].cumsum()}
240
+ {
241
+ "week": group["week"],
242
+ column.name: group[column.name].cumsum(),
243
+ }
200
244
  ),
201
245
  include_groups=False,
202
246
  )
@@ -204,16 +248,24 @@ def plot_year_cumulative(df: pd.DataFrame) -> str:
204
248
  )
205
249
 
206
250
  return (
207
- alt.Chart(year_cumulative, width=500, title="Cumultative Distance per Year")
251
+ alt.Chart(
252
+ year_cumulative,
253
+ width=500,
254
+ title=f"Cumulative {column.display_name} per Year",
255
+ )
208
256
  .mark_line()
209
257
  .encode(
210
258
  alt.X("week", title="Week"),
211
- alt.Y("distance_km", title="Distance / km"),
259
+ alt.Y(column.name, title=f"{column.display_name} / {column.unit}"),
212
260
  alt.Color("iso_year:N", title="Year"),
213
261
  [
214
262
  alt.Tooltip("week", title="Week"),
215
263
  alt.Tooltip("iso_year:N", title="Year"),
216
- alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
264
+ alt.Tooltip(
265
+ column.name,
266
+ title=f"{column.display_name} / {column.unit}",
267
+ format=column.format,
268
+ ),
217
269
  ],
218
270
  )
219
271
  .interactive()
@@ -221,24 +273,28 @@ def plot_year_cumulative(df: pd.DataFrame) -> str:
221
273
  )
222
274
 
223
275
 
224
- def tabulate_year_kind_mean(df: pd.DataFrame) -> pd.DataFrame:
276
+ def tabulate_year_kind_mean(
277
+ df: pd.DataFrame, column: ColumnDescription
278
+ ) -> pd.DataFrame:
225
279
  year_kind_mean = (
226
- df[["year", "kind", "distance_km", "hours"]]
280
+ df[["year", "kind", column.name, "hours"]]
227
281
  .groupby(["year", "kind"])
228
282
  .mean()
229
283
  .reset_index()
230
284
  )
231
285
 
232
286
  year_kind_mean_distance = year_kind_mean.pivot(
233
- index="year", columns="kind", values="distance_km"
287
+ index="year", columns="kind", values=column.name
234
288
  )
235
289
 
236
290
  return year_kind_mean_distance
237
291
 
238
292
 
239
- def plot_weekly_distance(df: pd.DataFrame, kind_scale: alt.Scale) -> str:
293
+ def plot_weekly_sums(
294
+ df: pd.DataFrame, column: ColumnDescription, kind_scale: alt.Scale
295
+ ) -> str:
240
296
  week_kind_total_distance = (
241
- df[["iso_year", "week", "kind", "distance_km"]]
297
+ df[["iso_year", "week", "kind", column.name]]
242
298
  .groupby(["iso_year", "week", "kind"])
243
299
  .sum()
244
300
  .reset_index()
@@ -260,17 +316,21 @@ def plot_weekly_distance(df: pd.DataFrame, kind_scale: alt.Scale) -> str:
260
316
  | (week_kind_total_distance["iso_year"] == last_year - 1)
261
317
  & (week_kind_total_distance["week"] >= last_week)
262
318
  ],
263
- title="Weekly Distance",
319
+ title=f"Weekly {column.display_name}",
264
320
  )
265
321
  .mark_bar()
266
322
  .encode(
267
323
  alt.X("year_week", title="Year and Week"),
268
- alt.Y("distance_km", title="Distance / km"),
324
+ alt.Y(column.name, title=f"{column.display_name} / {column.unit}"),
269
325
  alt.Color("kind", scale=kind_scale, title="Kind"),
270
326
  [
271
327
  alt.Tooltip("year_week", title="Year and Week"),
272
328
  alt.Tooltip("kind", title="Kind"),
273
- alt.Tooltip("distance_km", title="Distance / km", format=".1f"),
329
+ alt.Tooltip(
330
+ column.name,
331
+ title=f"{column.display_name} / {column.unit}",
332
+ format=column.format,
333
+ ),
274
334
  ],
275
335
  )
276
336
  .to_json(format="vega")
@@ -0,0 +1,37 @@
1
+ import dataclasses
2
+
3
+
4
+ @dataclasses.dataclass
5
+ class ColumnDescription:
6
+ name: str
7
+ display_name: str
8
+ unit: str
9
+ format: str
10
+
11
+
12
+ column_distance = ColumnDescription(
13
+ name="distance_km",
14
+ display_name="Distance",
15
+ unit="km",
16
+ format=".1f",
17
+ )
18
+
19
+ column_elevation = ColumnDescription(
20
+ name="elevation",
21
+ display_name="Elevation",
22
+ unit="m",
23
+ format=".0f",
24
+ )
25
+ column_elevation_gain = ColumnDescription(
26
+ name="elevation_gain",
27
+ display_name="Elevation Gain",
28
+ unit="m",
29
+ format=".0f",
30
+ )
31
+
32
+ column_speed = ColumnDescription(
33
+ name="speed",
34
+ display_name="Speed",
35
+ unit="km/h",
36
+ format=".1f",
37
+ )
@@ -32,6 +32,21 @@
32
32
  </select>
33
33
  </div>
34
34
 
35
+ <div class="mb-3">
36
+ <label for="" class="form-label">Tags</label>
37
+ <div class="form-control">
38
+ {% for tag in tags %}
39
+ <div class="form-check form-check-inline">
40
+ <input class="form-check-input" type="checkbox" name="tag" value="{{ tag.id }}" id="tag_{{ tag.id }}" {%
41
+ if tag in activity.tags %} checked {% endif %}>
42
+ <label class="form-check-label" for="tag_{{ tag.id }}">
43
+ {{ tag.tag }}
44
+ </label>
45
+ </div>
46
+ {% endfor %}
47
+ </div>
48
+ </div>
49
+
35
50
  <button type="submit" class="btn btn-primary">Save</button>
36
51
  </form>
37
52
 
@@ -11,13 +11,22 @@
11
11
  <div class="col-sm-12 col-md-4">
12
12
  <dl>
13
13
  <dt>Name</dt>
14
- <dd>{{ activity["name"] }}</dd>
14
+ <dd>{{ activity.name }}</dd>
15
15
 
16
16
  {% if activity.kind %}
17
17
  <dt>Kind</dt>
18
18
  <dd>{{ activity.kind.name }}</dd>
19
19
  {% endif %}
20
20
 
21
+ {% if activity.tags %}
22
+ <dt>Tags</dt>
23
+ <dd>
24
+ {% for tag in activity.tags %}
25
+ <span class="badge text-bg-primary">{{ tag.tag }}</span>
26
+ {% endfor %}
27
+ </dd>
28
+ {% endif %}
29
+
21
30
  <dt>Distance</dt>
22
31
  <dd>{{ activity.distance_km|round(1) }} km</dd>
23
32
 
@@ -49,7 +58,7 @@
49
58
 
50
59
  {% if activity.elevation_gain %}
51
60
  <dt>Elevation gain</dt>
52
- <dd>{{ activity.elevation_gain|round(0) }} m</dd>
61
+ <dd>{{ activity.elevation_gain|round(0)|int }} m</dd>
53
62
  {% endif %}
54
63
 
55
64
  {% if activity.equipment %}
@@ -105,10 +114,23 @@
105
114
  </style>
106
115
 
107
116
  <div>
108
- {% for speed, color in speed_color_bar.colors %}
109
- <span class="colorbar" style="width: 15px; background-color: {{ color }}">{{ speed }}</span>
117
+ {% for value, color in line_color_bar.colors %}
118
+ <span class="colorbar" style="width: 15px; background-color: {{ color }}">{{ value }}</span>
110
119
  {% endfor %}
111
- km/h
120
+ {{ line_color_columns_avail[line_color_column].unit }}
121
+ </div>
122
+
123
+ <div class="mb-3" style="padding-top: 10px;">
124
+ <form method="GET">
125
+ <label class="form-label">Line Color by</label>
126
+ <select class="form-select" aria-label="Line Color by" name="line_color_column"
127
+ onchange="this.form.submit()">
128
+ {% for name, column in line_color_columns_avail.items() %}
129
+ <option {% if name==line_color_column %} selected {% endif %} value="{{ name }}">{{
130
+ column.display_name }}</option>
131
+ {% endfor %}
132
+ </select>
133
+ </form>
112
134
  </div>
113
135
  </div>
114
136
  </div>
@@ -1,17 +1,33 @@
1
- <!-- filepath: /home/michael/Nextcloud/Python/Geoactivity-Fork/geo-activity-playground/geo_activity_playground/webui/templates/bubble_chart/index.html.j2 -->
2
1
  {% extends "page.html.j2" %}
3
2
 
4
3
  {% block container %}
4
+
5
+ <div class="row mb-3">
6
+ <div class="col">
7
+ <h1>Bubble Chart: Distance</h1>
8
+ <div id="bubble-chart-distance"></div>
9
+ <script>
10
+ const chartSpecDistance = {{ bubble_chart_distance | safe }};
11
+ vegaEmbed('#bubble-chart-distance', chartSpecDistance).then((result) => {
12
+ // Add a click event listener to the chart
13
+ result.view.addEventListener('click', (event, item) => {
14
+ if (item && item.datum && item.datum.activity_url) {
15
+ // Redirect to the activity URL
16
+ window.location.href = item.datum.activity_url;
17
+ }
18
+ });
19
+ }).catch(console.error);
20
+ </script>
21
+ </div>
22
+ </div>
23
+
5
24
  <div class="row mb-3">
6
25
  <div class="col">
7
- <h1>Bubble Chart</h1>
8
- <div id="bubble-chart"></div>
9
- <script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
10
- <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
11
- <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
26
+ <h1>Bubble Chart: Elevation Gain</h1>
27
+ <div id="bubble-chart-elevation-gain"></div>
12
28
  <script>
13
- const chartSpec = {{ bubble_chart | safe }};
14
- vegaEmbed('#bubble-chart', chartSpec).then((result) => {
29
+ const chartSpecElevationGain = {{ bubble_chart_elevation_gain | safe }};
30
+ vegaEmbed('#bubble-chart-elevation-gain', chartSpecElevationGain).then((result) => {
15
31
  // Add a click event listener to the chart
16
32
  result.view.addEventListener('click', (event, item) => {
17
33
  if (item && item.datum && item.datum.activity_url) {