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
@@ -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],
|
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":
|
178
|
-
|
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":
|
182
|
-
|
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=
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
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
|
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(
|
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(
|
151
|
+
f"sum({column.name})",
|
152
|
+
format=column.format,
|
153
|
+
title=f"Total {column.display_name} / {column.unit}",
|
134
154
|
),
|
135
|
-
alt.Tooltip("count(
|
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
|
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
|
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(
|
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("
|
188
|
+
alt.Tooltip("yearmonth(start)", title="Year and Month"),
|
189
|
+
alt.Tooltip("kind", title="Kind"),
|
164
190
|
alt.Tooltip(
|
165
|
-
"sum(
|
191
|
+
f"sum({column.name})",
|
192
|
+
format=column.format,
|
193
|
+
title=f"Total {column.display_name} / {column.unit}",
|
166
194
|
),
|
167
|
-
alt.Tooltip("count(
|
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
|
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
|
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(
|
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(
|
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",
|
236
|
+
df[["iso_year", "week", column.name]]
|
196
237
|
.groupby("iso_year")
|
197
238
|
.apply(
|
198
239
|
lambda group: pd.DataFrame(
|
199
|
-
{
|
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(
|
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(
|
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(
|
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(
|
276
|
+
def tabulate_year_kind_mean(
|
277
|
+
df: pd.DataFrame, column: ColumnDescription
|
278
|
+
) -> pd.DataFrame:
|
225
279
|
year_kind_mean = (
|
226
|
-
df[["year", "kind",
|
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=
|
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
|
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",
|
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
|
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(
|
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(
|
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
|
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
|
109
|
-
<span class="colorbar" style="width: 15px; background-color: {{ color }}">{{
|
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
|
-
|
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
|
14
|
-
vegaEmbed('#bubble-chart',
|
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) {
|