geo-activity-playground 0.45.0__py3-none-any.whl → 1.1.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 (75) hide show
  1. geo_activity_playground/alembic/versions/dc8073871da7_add_plotspec_group_by.py +28 -0
  2. geo_activity_playground/core/config.py +1 -0
  3. geo_activity_playground/core/datamodel.py +12 -0
  4. geo_activity_playground/core/export.py +129 -0
  5. geo_activity_playground/core/meta_search.py +1 -1
  6. geo_activity_playground/core/parametric_plot.py +101 -47
  7. geo_activity_playground/webui/app.py +10 -1
  8. geo_activity_playground/webui/authenticator.py +4 -2
  9. geo_activity_playground/webui/blueprints/activity_blueprint.py +11 -10
  10. geo_activity_playground/webui/blueprints/auth_blueprint.py +6 -2
  11. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +2 -1
  12. geo_activity_playground/webui/blueprints/calendar_blueprint.py +3 -2
  13. geo_activity_playground/webui/blueprints/eddington_blueprints.py +3 -2
  14. geo_activity_playground/webui/blueprints/entry_views.py +11 -11
  15. geo_activity_playground/webui/blueprints/equipment_blueprint.py +2 -1
  16. geo_activity_playground/webui/blueprints/explorer_blueprint.py +343 -197
  17. geo_activity_playground/webui/blueprints/export_blueprint.py +31 -0
  18. geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +79 -0
  19. geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +38 -19
  20. geo_activity_playground/webui/blueprints/summary_blueprint.py +114 -240
  21. geo_activity_playground/webui/blueprints/upload_blueprint.py +9 -0
  22. geo_activity_playground/webui/columns.py +40 -7
  23. geo_activity_playground/webui/static/{browserconfig.xml → favicons/browserconfig.xml} +1 -1
  24. geo_activity_playground/webui/static/{site.webmanifest → favicons/site.webmanifest} +2 -2
  25. geo_activity_playground/webui/static/server-side-explorer.js +60 -0
  26. geo_activity_playground/webui/templates/activity/name.html.j2 +4 -4
  27. geo_activity_playground/webui/templates/activity/show.html.j2 +8 -8
  28. geo_activity_playground/webui/templates/auth/index.html.j2 +1 -0
  29. geo_activity_playground/webui/templates/eddington/distance.html.j2 +3 -3
  30. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +3 -3
  31. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +3 -3
  32. geo_activity_playground/webui/templates/equipment/index.html.j2 +1 -1
  33. geo_activity_playground/webui/templates/explorer/server-side.html.j2 +42 -36
  34. geo_activity_playground/webui/templates/export/index.html.j2 +39 -0
  35. geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +58 -0
  36. geo_activity_playground/webui/templates/home.html.j2 +1 -4
  37. geo_activity_playground/webui/templates/page.html.j2 +26 -43
  38. geo_activity_playground/webui/templates/plot-macros.html.j2 +72 -0
  39. geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +12 -7
  40. geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +24 -0
  41. geo_activity_playground/webui/templates/plot_builder/index.html.j2 +5 -0
  42. geo_activity_playground/webui/templates/summary/index.html.j2 +23 -230
  43. geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +3 -0
  44. {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/METADATA +2 -1
  45. {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/RECORD +74 -65
  46. geo_activity_playground/webui/templates/explorer/index.html.j2 +0 -148
  47. /geo_activity_playground/webui/static/{bootstrap-dark-mode.js → bootstrap/bootstrap-dark-mode.js} +0 -0
  48. /geo_activity_playground/webui/static/{bootstrap.bundle.min.js → bootstrap/bootstrap.bundle.min.js} +0 -0
  49. /geo_activity_playground/webui/static/{bootstrap.min.css → bootstrap/bootstrap.min.css} +0 -0
  50. /geo_activity_playground/webui/static/{android-chrome-192x192.png → favicons/android-chrome-192x192.png} +0 -0
  51. /geo_activity_playground/webui/static/{android-chrome-512x512.png → favicons/android-chrome-512x512.png} +0 -0
  52. /geo_activity_playground/webui/static/{apple-touch-icon.png → favicons/apple-touch-icon.png} +0 -0
  53. /geo_activity_playground/webui/static/{favicon-16x16.png → favicons/favicon-16x16.png} +0 -0
  54. /geo_activity_playground/webui/static/{favicon-32x32.png → favicons/favicon-32x32.png} +0 -0
  55. /geo_activity_playground/webui/static/{favicon-48x48.png → favicons/favicon-48x48.png} +0 -0
  56. /geo_activity_playground/webui/static/{favicon.ico → favicons/favicon.ico} +0 -0
  57. /geo_activity_playground/webui/static/{favicon.svg → favicons/favicon.svg} +0 -0
  58. /geo_activity_playground/webui/static/{mstile-150x150.png → favicons/mstile-150x150.png} +0 -0
  59. /geo_activity_playground/webui/static/{web-app-manifest-192x192.png → favicons/web-app-manifest-192x192.png} +0 -0
  60. /geo_activity_playground/webui/static/{web-app-manifest-512x512.png → favicons/web-app-manifest-512x512.png} +0 -0
  61. /geo_activity_playground/webui/static/{Leaflet.fullscreen.min.js → leaflet/Leaflet.fullscreen.min.js} +0 -0
  62. /geo_activity_playground/webui/static/{MarkerCluster.Default.css → leaflet/MarkerCluster.Default.css} +0 -0
  63. /geo_activity_playground/webui/static/{MarkerCluster.css → leaflet/MarkerCluster.css} +0 -0
  64. /geo_activity_playground/webui/static/{fullscreen.png → leaflet/fullscreen.png} +0 -0
  65. /geo_activity_playground/webui/static/{fullscreen@2x.png → leaflet/fullscreen@2x.png} +0 -0
  66. /geo_activity_playground/webui/static/{leaflet.css → leaflet/leaflet.css} +0 -0
  67. /geo_activity_playground/webui/static/{leaflet.fullscreen.css → leaflet/leaflet.fullscreen.css} +0 -0
  68. /geo_activity_playground/webui/static/{leaflet.js → leaflet/leaflet.js} +0 -0
  69. /geo_activity_playground/webui/static/{leaflet.markercluster.js → leaflet/leaflet.markercluster.js} +0 -0
  70. /geo_activity_playground/webui/static/{vega-embed@6 → vega/vega-embed@6.js} +0 -0
  71. /geo_activity_playground/webui/static/{vega-lite@4 → vega/vega-lite@4.js} +0 -0
  72. /geo_activity_playground/webui/static/{vega@5 → vega/vega@5.js} +0 -0
  73. {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/LICENSE +0 -0
  74. {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/WHEEL +0 -0
  75. {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,79 @@
1
+ import collections
2
+
3
+ import pandas as pd
4
+ from flask import Blueprint
5
+ from flask import render_template
6
+ from flask import request
7
+
8
+ from ...core.activities import ActivityRepository
9
+ from ...core.activities import make_geojson_from_time_series
10
+ from ...core.meta_search import apply_search_query
11
+ from ..search_util import search_query_from_form
12
+ from ..search_util import SearchQueryHistory
13
+
14
+
15
+ def make_hall_of_fame_blueprint(
16
+ repository: ActivityRepository,
17
+ search_query_history: SearchQueryHistory,
18
+ ) -> Blueprint:
19
+ blueprint = Blueprint("hall_of_fame", __name__, template_folder="templates")
20
+
21
+ @blueprint.route("/")
22
+ def index() -> str:
23
+ query = search_query_from_form(request.args)
24
+ search_query_history.register_query(query)
25
+ activities = apply_search_query(repository.meta, query)
26
+ df = activities
27
+
28
+ nominations = nominate_activities(df)
29
+
30
+ return render_template(
31
+ "hall_of_fame/index.html.j2",
32
+ nominations=[
33
+ (
34
+ repository.get_activity_by_id(activity_id),
35
+ reasons,
36
+ make_geojson_from_time_series(
37
+ repository.get_time_series(activity_id)
38
+ ),
39
+ )
40
+ for activity_id, reasons in nominations.items()
41
+ ],
42
+ query=query.to_jinja(),
43
+ )
44
+
45
+ return blueprint
46
+
47
+
48
+ def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
49
+ nominations: dict[int, list[str]] = collections.defaultdict(list)
50
+
51
+ _nominate_activities_inner(meta, "", nominations)
52
+
53
+ for kind, group in meta.groupby("kind"):
54
+ _nominate_activities_inner(group, f" for {kind}", nominations)
55
+ for equipment, group in meta.groupby("equipment"):
56
+ _nominate_activities_inner(group, f" with {equipment}", nominations)
57
+
58
+ return nominations
59
+
60
+
61
+ def _nominate_activities_inner(
62
+ meta: pd.DataFrame, title_suffix: str, nominations: dict[int, list[str]]
63
+ ) -> None:
64
+ ratings = [
65
+ ("distance_km", "Greatest distance", "{:.1f} km"),
66
+ ("elapsed_time", "Longest elapsed time", "{}"),
67
+ ("average_speed_moving_kmh", "Highest average moving speed", "{:.1f} km/h"),
68
+ ("average_speed_elapsed_kmh", "Highest average elapsed speed", "{:.1f} km/h"),
69
+ ("calories", "Most calories burnt", "{:.0f}"),
70
+ ("steps", "Most steps", "{:.0f}"),
71
+ ("elevation_gain", "Largest elevation gain", "{:.0f} m"),
72
+ ]
73
+
74
+ for variable, title, format_str in ratings:
75
+ if variable in meta.columns and not pd.isna(meta[variable]).all():
76
+ i = meta[variable].idxmax()
77
+ value = meta.loc[i, variable]
78
+ format_applied = format_str.format(value)
79
+ nominations[i].append(f"{title}{title_suffix}: {format_applied}")
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  import sqlalchemy
2
4
  from flask import Blueprint
3
5
  from flask import redirect
@@ -5,9 +7,12 @@ from flask import render_template
5
7
  from flask import request
6
8
  from flask import Response
7
9
  from flask import url_for
10
+ from flask.typing import ResponseReturnValue
11
+ from flask.typing import RouteCallable
8
12
 
9
13
  from ...core.activities import ActivityRepository
10
14
  from ...core.datamodel import DB
15
+ from ...core.parametric_plot import GROUP_BY_VARIABLES
11
16
  from ...core.parametric_plot import make_parametric_plot
12
17
  from ...core.parametric_plot import MARKS
13
18
  from ...core.parametric_plot import PlotSpec
@@ -25,7 +30,7 @@ def make_plot_builder_blueprint(
25
30
  blueprint = Blueprint("plot_builder", __name__, template_folder="templates")
26
31
 
27
32
  @blueprint.route("/")
28
- def index() -> Response:
33
+ def index() -> ResponseReturnValue:
29
34
  return render_template(
30
35
  "plot_builder/index.html.j2",
31
36
  specs=DB.session.scalars(sqlalchemy.select(PlotSpec)).all(),
@@ -33,7 +38,7 @@ def make_plot_builder_blueprint(
33
38
 
34
39
  @blueprint.route("/new")
35
40
  @needs_authentication(authenticator)
36
- def new() -> Response:
41
+ def new() -> ResponseReturnValue:
37
42
  spec = PlotSpec(
38
43
  name="My New Plot",
39
44
  mark="bar",
@@ -45,23 +50,36 @@ def make_plot_builder_blueprint(
45
50
  DB.session.commit()
46
51
  return redirect(url_for(".edit", id=spec.id))
47
52
 
48
- @blueprint.route("/edit/<int:id>")
53
+ @blueprint.route("/import-spec", methods=["GET", "POST"])
49
54
  @needs_authentication(authenticator)
50
- def edit(id: int) -> Response:
51
- spec = DB.session.get(PlotSpec, id)
52
- if request.args:
53
- spec.name = request.args["name"]
54
- spec.mark = request.args["mark"]
55
- spec.x = request.args["x"]
56
- spec.y = request.args["y"]
57
- spec.color = request.args["color"]
58
- spec.shape = request.args["shape"]
59
- spec.size = request.args["size"]
60
- spec.size = request.args["size"]
61
- spec.row = request.args["row"]
62
- spec.column = request.args["column"]
63
- spec.facet = request.args["facet"]
64
- spec.opacity = request.args["opacity"]
55
+ def import_spec() -> ResponseReturnValue:
56
+ if request.form:
57
+ parameters = json.loads(request.form["spec_json"])
58
+ spec = PlotSpec(**parameters)
59
+ DB.session.add(spec)
60
+ DB.session.commit()
61
+ return redirect(url_for(".edit", id=spec.id))
62
+ else:
63
+ return render_template("plot_builder/import-spec.html.j2")
64
+
65
+ @blueprint.route("/edit/<int:id>", methods=["GET", "POST"])
66
+ @needs_authentication(authenticator)
67
+ def edit(id: int) -> ResponseReturnValue:
68
+ spec = DB.session.get_one(PlotSpec, id)
69
+ if request.form:
70
+ spec.name = request.form["name"]
71
+ spec.mark = request.form["mark"]
72
+ spec.x = request.form["x"]
73
+ spec.y = request.form["y"]
74
+ spec.color = request.form["color"]
75
+ spec.shape = request.form["shape"]
76
+ spec.size = request.form["size"]
77
+ spec.size = request.form["size"]
78
+ spec.row = request.form["row"]
79
+ spec.column = request.form["column"]
80
+ spec.facet = request.form["facet"]
81
+ spec.opacity = request.form["opacity"]
82
+ spec.group_by = request.form["group_by"]
65
83
  try:
66
84
  plot = make_parametric_plot(repository.meta, spec)
67
85
  DB.session.commit()
@@ -73,13 +91,14 @@ def make_plot_builder_blueprint(
73
91
  marks=MARKS,
74
92
  discrete=VARIABLES_1,
75
93
  continuous=VARIABLES_2,
94
+ group_by=GROUP_BY_VARIABLES,
76
95
  plot=plot,
77
96
  spec=spec,
78
97
  )
79
98
 
80
99
  @blueprint.route("/delete/<int:id>")
81
100
  @needs_authentication(authenticator)
82
- def delete(id: int) -> Response:
101
+ def delete(id: int) -> ResponseReturnValue:
83
102
  spec = DB.session.get(PlotSpec, id)
84
103
  DB.session.delete(spec)
85
104
  flasher.flash_message(f"Deleted plot '{spec.name}'.", FlashTypes.SUCCESS)
@@ -1,4 +1,3 @@
1
- import collections
2
1
  import datetime
3
2
 
4
3
  import altair as alt
@@ -9,224 +8,41 @@ from flask import render_template
9
8
  from flask import request
10
9
 
11
10
  from ...core.activities import ActivityRepository
12
- from ...core.activities import make_geojson_from_time_series
13
11
  from ...core.config import Config
14
12
  from ...core.datamodel import DB
15
13
  from ...core.datamodel import PlotSpec
16
14
  from ...core.meta_search import apply_search_query
17
15
  from ...core.parametric_plot import make_parametric_plot
18
- from ..columns import column_distance
19
- from ..columns import column_elevation_gain
20
16
  from ..columns import ColumnDescription
17
+ from ..columns import META_COLUMNS
21
18
  from ..plot_util import make_kind_scale
22
19
  from ..search_util import search_query_from_form
23
20
  from ..search_util import SearchQueryHistory
24
21
 
25
22
 
26
- def make_summary_blueprint(
27
- repository: ActivityRepository,
28
- config: Config,
29
- search_query_history: SearchQueryHistory,
30
- ) -> Blueprint:
31
- blueprint = Blueprint("summary", __name__, template_folder="templates")
32
-
33
- @blueprint.route("/")
34
- def index():
35
- query = search_query_from_form(request.args)
36
- search_query_history.register_query(query)
37
- activities = apply_search_query(repository.meta, query)
38
-
39
- kind_scale = make_kind_scale(repository.meta, config)
40
- df = activities
41
-
42
- nominations = nominate_activities(df)
43
-
44
- return render_template(
45
- "summary/index.html.j2",
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)
63
- .reset_index()
64
- .to_dict(orient="split"),
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
- ),
74
- nominations=[
75
- (
76
- repository.get_activity_by_id(activity_id),
77
- reasons,
78
- make_geojson_from_time_series(
79
- repository.get_time_series(activity_id)
80
- ),
81
- )
82
- for activity_id, reasons in nominations.items()
83
- ],
84
- query=query.to_jinja(),
85
- custom_plots=[
86
- (spec, make_parametric_plot(repository.meta, spec))
87
- for spec in DB.session.scalars(sqlalchemy.select(PlotSpec)).all()
88
- ],
89
- )
90
-
91
- return blueprint
92
-
93
-
94
- def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
95
- nominations: dict[int, list[str]] = collections.defaultdict(list)
96
-
97
- _nominate_activities_inner(meta, "", nominations)
98
-
99
- for kind, group in meta.groupby("kind"):
100
- _nominate_activities_inner(group, f" for {kind}", nominations)
101
- for equipment, group in meta.groupby("equipment"):
102
- _nominate_activities_inner(group, f" with {equipment}", nominations)
103
-
104
- return nominations
105
-
106
-
107
- def _nominate_activities_inner(
108
- meta: pd.DataFrame, title_suffix: str, nominations: dict[int, list[str]]
109
- ) -> None:
110
- ratings = [
111
- ("distance_km", "Greatest distance", "{:.1f} km"),
112
- ("elapsed_time", "Longest elapsed time", "{}"),
113
- ("average_speed_moving_kmh", "Highest average moving speed", "{:.1f} km/h"),
114
- ("average_speed_elapsed_kmh", "Highest average elapsed speed", "{:.1f} km/h"),
115
- ("calories", "Most calories burnt", "{:.0f}"),
116
- ("steps", "Most steps", "{:.0f}"),
117
- ("elevation_gain", "Largest elevation gain", "{:.0f} m"),
118
- ]
119
-
120
- for variable, title, format_str in ratings:
121
- if variable in meta.columns and not pd.isna(meta[variable]).all():
122
- i = meta[variable].idxmax()
123
- value = meta.loc[i, variable]
124
- format_applied = format_str.format(value)
125
- nominations[i].append(f"{title}{title_suffix}: {format_applied}")
126
-
127
-
128
- def plot_heatmaps(
129
- meta: pd.DataFrame, column: ColumnDescription, config: Config
130
- ) -> dict[int, str]:
131
- return {
132
- year: alt.Chart(
133
- meta.loc[(meta["year"] == year)],
134
- title=f"Daily {column.display_name} Heatmap",
135
- )
136
- .mark_rect()
137
- .encode(
138
- alt.X("date(start):O", title="Day of month"),
139
- alt.Y(
140
- "yearmonth(start):O",
141
- # scale=alt.Scale(reverse=True),
142
- title="Year and month",
143
- ),
144
- alt.Color(
145
- f"sum({column.name})",
146
- scale=alt.Scale(scheme=config.color_scheme_for_counts),
147
- ),
148
- [
149
- alt.Tooltip("yearmonthdate(start)", title="Date"),
150
- alt.Tooltip(
151
- f"sum({column.name})",
152
- format=column.format,
153
- title=f"Total {column.display_name} / {column.unit}",
154
- ),
155
- alt.Tooltip(f"count({column.name})", title="Number of activities"),
156
- ],
157
- )
158
- .to_json(format="vega")
159
- for year in sorted(meta["year"].unique())
160
- }
161
-
162
-
163
- def plot_monthly_sums(
164
- meta: pd.DataFrame, column: ColumnDescription, kind_scale: alt.Scale
165
- ) -> str:
23
+ def plot_per_year_per_kind(df: pd.DataFrame, column: ColumnDescription) -> str:
166
24
  return (
167
25
  alt.Chart(
168
- meta.loc[
169
- (
170
- meta["start"]
171
- >= pd.to_datetime(
172
- datetime.datetime.now() - datetime.timedelta(days=2 * 365)
173
- )
174
- )
175
- ],
176
- title=f"Monthly {column.display_name}",
26
+ df,
27
+ title=f"{column.display_name} per Year",
177
28
  )
178
29
  .mark_bar()
179
30
  .encode(
180
- alt.X("month(start)", title="Month"),
31
+ alt.X("year:O", title="Year"),
181
32
  alt.Y(
182
- f"sum({column.name})",
183
- title=f"{column.display_name} / {column.unit}",
33
+ f"sum({column.name})", title=f"{column.display_name} / {column.unit}"
184
34
  ),
185
- alt.Color("kind", scale=kind_scale, title="Kind"),
186
- alt.Column("year(start):O", title="Year"),
35
+ alt.Color("kind", title="Kind"),
187
36
  [
188
- alt.Tooltip("yearmonth(start)", title="Year and Month"),
37
+ alt.Tooltip("year", title="Year"),
189
38
  alt.Tooltip("kind", title="Kind"),
190
39
  alt.Tooltip(
191
40
  f"sum({column.name})",
192
- format=column.format,
193
- title=f"Total {column.display_name} / {column.unit}",
194
- ),
195
- alt.Tooltip(f"count({column.name})", title="Number of activities"),
196
- ],
197
- )
198
- .resolve_axis(x="independent")
199
- .to_json(format="vega")
200
- )
201
-
202
-
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
-
213
- return (
214
- alt.Chart(year_kind_total, title=f"Total {column.display_name} per Year")
215
- .mark_bar()
216
- .encode(
217
- alt.X("year:O", title="Year"),
218
- alt.Y(column.name, title=f"{column.display_name} / {column.unit}"),
219
- alt.Color("kind", scale=kind_scale, title="Kind"),
220
- [
221
- alt.Tooltip("year:O", title="Year"),
222
- alt.Tooltip("kind", title="Kind"),
223
- alt.Tooltip(
224
- column.name,
225
41
  title=f"{column.display_name} / {column.unit}",
226
- format=column.format,
227
42
  ),
228
43
  ],
229
44
  )
45
+ .interactive()
230
46
  .to_json(format="vega")
231
47
  )
232
48
 
@@ -250,7 +66,6 @@ def plot_year_cumulative(df: pd.DataFrame, column: ColumnDescription) -> str:
250
66
  return (
251
67
  alt.Chart(
252
68
  year_cumulative,
253
- width=500,
254
69
  title=f"Cumulative {column.display_name} per Year",
255
70
  )
256
71
  .mark_line()
@@ -273,65 +88,124 @@ def plot_year_cumulative(df: pd.DataFrame, column: ColumnDescription) -> str:
273
88
  )
274
89
 
275
90
 
276
- def tabulate_year_kind_mean(
277
- df: pd.DataFrame, column: ColumnDescription
278
- ) -> pd.DataFrame:
279
- year_kind_mean = (
280
- df[["year", "kind", column.name, "hours"]]
281
- .groupby(["year", "kind"])
282
- .mean()
283
- .reset_index()
284
- )
285
-
286
- year_kind_mean_distance = year_kind_mean.pivot(
287
- index="year", columns="kind", values=column.name
288
- )
289
-
290
- return year_kind_mean_distance
291
-
292
-
293
- def plot_weekly_sums(
294
- df: pd.DataFrame, column: ColumnDescription, kind_scale: alt.Scale
295
- ) -> str:
296
- week_kind_total_distance = (
297
- df[["iso_year", "week", "kind", column.name]]
298
- .groupby(["iso_year", "week", "kind"])
299
- .sum()
300
- .reset_index()
301
- )
302
- week_kind_total_distance["year_week"] = [
303
- f"{year}-{week:02d}"
304
- for year, week in zip(
305
- week_kind_total_distance["iso_year"], week_kind_total_distance["week"]
91
+ def plot_per_iso_week(df: pd.DataFrame, column: ColumnDescription) -> str:
92
+ return (
93
+ alt.Chart(
94
+ df,
95
+ title=f"{column.display_name} per Week",
306
96
  )
307
- ]
97
+ .mark_circle()
98
+ .encode(
99
+ alt.X("week:O", title="ISO Week"),
100
+ alt.Y("iso_year:O", title="ISO Year"),
101
+ alt.Size(
102
+ f"sum({column.name})", title=f"{column.display_name} / {column.unit}"
103
+ ),
104
+ [
105
+ alt.Tooltip("iso_year", title="ISO Year"),
106
+ alt.Tooltip("week", title="ISO Week"),
107
+ alt.Tooltip(
108
+ f"sum({column.name})",
109
+ title=f"{column.display_name} / {column.unit}",
110
+ format=column.format,
111
+ ),
112
+ ],
113
+ )
114
+ .interactive()
115
+ .to_json(format="vega")
116
+ )
308
117
 
309
- last_year = week_kind_total_distance["iso_year"].iloc[-1]
310
- last_week = week_kind_total_distance["week"].iloc[-1]
311
118
 
119
+ def heatmap_per_day(df: pd.DataFrame, column: ColumnDescription) -> str:
312
120
  return (
313
121
  alt.Chart(
314
- week_kind_total_distance.loc[
315
- (week_kind_total_distance["iso_year"] == last_year)
316
- | (week_kind_total_distance["iso_year"] == last_year - 1)
317
- & (week_kind_total_distance["week"] >= last_week)
318
- ],
319
- title=f"Weekly {column.display_name}",
122
+ _filter_past_year(df),
123
+ title=f"{column.display_name} per day",
320
124
  )
321
- .mark_bar()
125
+ .mark_rect()
322
126
  .encode(
323
- alt.X("year_week", title="Year and Week"),
324
- alt.Y(column.name, title=f"{column.display_name} / {column.unit}"),
325
- alt.Color("kind", scale=kind_scale, title="Kind"),
127
+ alt.X("iso_year_week:O", title="ISO Year and Week"),
128
+ alt.Y(
129
+ "iso_day:O",
130
+ # scale=alt.Scale(
131
+ # domain=list(range(1, 8)),
132
+ # range=[
133
+ # "Monday",
134
+ # "Tuesday",
135
+ # "Wednesday",
136
+ # "Thursday",
137
+ # "Friday",
138
+ # "Saturday",
139
+ # "Sunday",
140
+ # ],
141
+ # ),
142
+ title="ISO Weekday",
143
+ ),
144
+ alt.Color(
145
+ f"sum({column.name})",
146
+ scale=alt.Scale(scheme="viridis"),
147
+ title=f"{column.display_name} / {column.unit}",
148
+ ),
326
149
  [
327
- alt.Tooltip("year_week", title="Year and Week"),
328
- alt.Tooltip("kind", title="Kind"),
150
+ alt.Tooltip("iso_year_week", title="ISO Year and Week"),
151
+ alt.Tooltip("iso_day", title="ISO Day"),
329
152
  alt.Tooltip(
330
- column.name,
153
+ f"sum({column.name})",
331
154
  title=f"{column.display_name} / {column.unit}",
332
155
  format=column.format,
333
156
  ),
334
157
  ],
335
158
  )
159
+ .interactive()
336
160
  .to_json(format="vega")
337
161
  )
162
+
163
+
164
+ def _filter_past_year(df: pd.DataFrame) -> pd.DataFrame:
165
+ now = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
166
+ start = now - datetime.timedelta(days=365)
167
+ return df.loc[df["start"] >= start]
168
+
169
+
170
+ def make_summary_blueprint(
171
+ repository: ActivityRepository,
172
+ config: Config,
173
+ search_query_history: SearchQueryHistory,
174
+ ) -> Blueprint:
175
+ blueprint = Blueprint("summary", __name__, template_folder="templates")
176
+
177
+ @blueprint.route("/")
178
+ def index():
179
+ query = search_query_from_form(request.args)
180
+ search_query_history.register_query(query)
181
+ activities = apply_search_query(repository.meta, query)
182
+
183
+ kind_scale = make_kind_scale(repository.meta, config)
184
+ df = activities
185
+
186
+ return render_template(
187
+ "summary/index.html.j2",
188
+ query=query.to_jinja(),
189
+ custom_plots=[
190
+ (spec, make_parametric_plot(repository.meta, spec))
191
+ for spec in DB.session.scalars(sqlalchemy.select(PlotSpec)).all()
192
+ ],
193
+ plot_per_year_per_kind={
194
+ column.display_name: plot_per_year_per_kind(df, column)
195
+ for column in META_COLUMNS
196
+ },
197
+ plot_per_year_cumulative={
198
+ column.display_name: plot_year_cumulative(df, column)
199
+ for column in META_COLUMNS
200
+ },
201
+ plot_per_iso_week={
202
+ column.display_name: plot_per_iso_week(df, column)
203
+ for column in META_COLUMNS
204
+ },
205
+ heatmap_per_day={
206
+ column.display_name: heatmap_per_day(df, column)
207
+ for column in META_COLUMNS
208
+ },
209
+ )
210
+
211
+ return blueprint
@@ -25,6 +25,8 @@ from ...importers.strava_api import import_from_strava_api
25
25
  from ...importers.strava_checkout import import_from_strava_checkout
26
26
  from ..authenticator import Authenticator
27
27
  from ..authenticator import needs_authentication
28
+ from ..flasher import Flasher
29
+ from ..flasher import FlashTypes
28
30
 
29
31
 
30
32
  def make_upload_blueprint(
@@ -32,6 +34,7 @@ def make_upload_blueprint(
32
34
  tile_visit_accessor: TileVisitAccessor,
33
35
  config: Config,
34
36
  authenticator: Authenticator,
37
+ flasher: Flasher,
35
38
  ) -> Blueprint:
36
39
  blueprint = Blueprint("upload", __name__, template_folder="templates")
37
40
 
@@ -72,6 +75,12 @@ def make_upload_blueprint(
72
75
  ".tcx",
73
76
  ]
74
77
  assert target_path.is_relative_to("Activities")
78
+ if target_path.exists():
79
+ flasher.flash_message(
80
+ f"An activity with path '{target_path}' already exists. Rename the file and try again.",
81
+ FlashTypes.DANGER,
82
+ )
83
+ return redirect(url_for(".index"))
75
84
  file.save(target_path)
76
85
  scan_for_activities(
77
86
  repository,