geo-activity-playground 1.0.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.
- geo_activity_playground/alembic/versions/dc8073871da7_add_plotspec_group_by.py +28 -0
- geo_activity_playground/core/config.py +1 -0
- geo_activity_playground/core/datamodel.py +9 -0
- geo_activity_playground/core/parametric_plot.py +101 -47
- geo_activity_playground/webui/app.py +7 -0
- geo_activity_playground/webui/blueprints/activity_blueprint.py +11 -10
- geo_activity_playground/webui/blueprints/auth_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/calendar_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/eddington_blueprints.py +3 -2
- geo_activity_playground/webui/blueprints/entry_views.py +11 -11
- geo_activity_playground/webui/blueprints/equipment_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +47 -13
- geo_activity_playground/webui/blueprints/export_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +79 -0
- geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +38 -19
- geo_activity_playground/webui/blueprints/summary_blueprint.py +114 -240
- geo_activity_playground/webui/columns.py +40 -7
- geo_activity_playground/webui/static/{browserconfig.xml → favicons/browserconfig.xml} +1 -1
- geo_activity_playground/webui/static/{site.webmanifest → favicons/site.webmanifest} +2 -2
- geo_activity_playground/webui/static/server-side-explorer.js +7 -2
- geo_activity_playground/webui/templates/activity/name.html.j2 +4 -4
- geo_activity_playground/webui/templates/activity/show.html.j2 +8 -8
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +3 -3
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +3 -3
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +3 -3
- geo_activity_playground/webui/templates/equipment/index.html.j2 +1 -1
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +5 -4
- geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +58 -0
- geo_activity_playground/webui/templates/home.html.j2 +1 -4
- geo_activity_playground/webui/templates/page.html.j2 +23 -37
- geo_activity_playground/webui/templates/plot-macros.html.j2 +72 -0
- geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +12 -7
- geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +24 -0
- geo_activity_playground/webui/templates/plot_builder/index.html.j2 +5 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +23 -230
- geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +3 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.1.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.1.0.dist-info}/RECORD +68 -62
- /geo_activity_playground/webui/static/{bootstrap-dark-mode.js → bootstrap/bootstrap-dark-mode.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.bundle.min.js → bootstrap/bootstrap.bundle.min.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.min.css → bootstrap/bootstrap.min.css} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-192x192.png → favicons/android-chrome-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-512x512.png → favicons/android-chrome-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{apple-touch-icon.png → favicons/apple-touch-icon.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-16x16.png → favicons/favicon-16x16.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-32x32.png → favicons/favicon-32x32.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-48x48.png → favicons/favicon-48x48.png} +0 -0
- /geo_activity_playground/webui/static/{favicon.ico → favicons/favicon.ico} +0 -0
- /geo_activity_playground/webui/static/{favicon.svg → favicons/favicon.svg} +0 -0
- /geo_activity_playground/webui/static/{mstile-150x150.png → favicons/mstile-150x150.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-192x192.png → favicons/web-app-manifest-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-512x512.png → favicons/web-app-manifest-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{Leaflet.fullscreen.min.js → leaflet/Leaflet.fullscreen.min.js} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.Default.css → leaflet/MarkerCluster.Default.css} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.css → leaflet/MarkerCluster.css} +0 -0
- /geo_activity_playground/webui/static/{fullscreen.png → leaflet/fullscreen.png} +0 -0
- /geo_activity_playground/webui/static/{fullscreen@2x.png → leaflet/fullscreen@2x.png} +0 -0
- /geo_activity_playground/webui/static/{leaflet.css → leaflet/leaflet.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.fullscreen.css → leaflet/leaflet.fullscreen.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.js → leaflet/leaflet.js} +0 -0
- /geo_activity_playground/webui/static/{leaflet.markercluster.js → leaflet/leaflet.markercluster.js} +0 -0
- /geo_activity_playground/webui/static/{vega-embed@6 → vega/vega-embed@6.js} +0 -0
- /geo_activity_playground/webui/static/{vega-lite@4 → vega/vega-lite@4.js} +0 -0
- /geo_activity_playground/webui/static/{vega@5 → vega/vega@5.js} +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.1.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.1.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-1.0.0.dist-info → geo_activity_playground-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@ from flask import Blueprint
|
|
2
2
|
from flask import render_template
|
3
3
|
from flask import request
|
4
4
|
from flask import Response
|
5
|
+
from flask.typing import ResponseReturnValue
|
5
6
|
|
6
7
|
from ...core.export import export_all
|
7
8
|
from ..authenticator import Authenticator
|
@@ -13,12 +14,12 @@ def make_export_blueprint(authenticator: Authenticator) -> Blueprint:
|
|
13
14
|
|
14
15
|
@needs_authentication(authenticator)
|
15
16
|
@blueprint.route("/")
|
16
|
-
def index():
|
17
|
+
def index() -> str:
|
17
18
|
return render_template("export/index.html.j2")
|
18
19
|
|
19
20
|
@needs_authentication(authenticator)
|
20
21
|
@blueprint.route("/export")
|
21
|
-
def export():
|
22
|
+
def export() -> Response:
|
22
23
|
meta_format = request.args["meta_format"]
|
23
24
|
activity_format = request.args["activity_format"]
|
24
25
|
return Response(
|
@@ -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() ->
|
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() ->
|
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("/
|
53
|
+
@blueprint.route("/import-spec", methods=["GET", "POST"])
|
49
54
|
@needs_authentication(authenticator)
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
spec
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
spec.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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) ->
|
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
|
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
|
-
|
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("
|
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",
|
186
|
-
alt.Column("year(start):O", title="Year"),
|
35
|
+
alt.Color("kind", title="Kind"),
|
187
36
|
[
|
188
|
-
alt.Tooltip("
|
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
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
315
|
-
|
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
|
-
.
|
125
|
+
.mark_rect()
|
322
126
|
.encode(
|
323
|
-
alt.X("
|
324
|
-
alt.Y(
|
325
|
-
|
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("
|
328
|
-
alt.Tooltip("
|
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
|
@@ -15,19 +15,44 @@ column_distance = ColumnDescription(
|
|
15
15
|
unit="km",
|
16
16
|
format=".1f",
|
17
17
|
)
|
18
|
-
|
19
|
-
column_elevation = ColumnDescription(
|
20
|
-
name="elevation",
|
21
|
-
display_name="Elevation",
|
22
|
-
unit="m",
|
23
|
-
format=".0f",
|
24
|
-
)
|
25
18
|
column_elevation_gain = ColumnDescription(
|
26
19
|
name="elevation_gain",
|
27
20
|
display_name="Elevation Gain",
|
28
21
|
unit="m",
|
29
22
|
format=".0f",
|
30
23
|
)
|
24
|
+
column_hours = ColumnDescription(
|
25
|
+
name="hours",
|
26
|
+
display_name="Elapsed time",
|
27
|
+
unit="h",
|
28
|
+
format=".1f",
|
29
|
+
)
|
30
|
+
column_hours_moving = ColumnDescription(
|
31
|
+
name="hours_moving",
|
32
|
+
display_name="Moving time",
|
33
|
+
unit="h",
|
34
|
+
format=".1f",
|
35
|
+
)
|
36
|
+
column_calories = ColumnDescription(
|
37
|
+
name="calories",
|
38
|
+
display_name="Energy",
|
39
|
+
unit="kcal",
|
40
|
+
format=".1f",
|
41
|
+
)
|
42
|
+
column_steps = ColumnDescription(
|
43
|
+
name="steps",
|
44
|
+
display_name="Steps",
|
45
|
+
unit="1",
|
46
|
+
format=".1f",
|
47
|
+
)
|
48
|
+
META_COLUMNS = [
|
49
|
+
column_distance,
|
50
|
+
column_elevation_gain,
|
51
|
+
column_hours,
|
52
|
+
column_hours_moving,
|
53
|
+
column_calories,
|
54
|
+
column_steps,
|
55
|
+
]
|
31
56
|
|
32
57
|
column_speed = ColumnDescription(
|
33
58
|
name="speed",
|
@@ -35,3 +60,11 @@ column_speed = ColumnDescription(
|
|
35
60
|
unit="km/h",
|
36
61
|
format=".1f",
|
37
62
|
)
|
63
|
+
column_elevation = ColumnDescription(
|
64
|
+
name="elevation",
|
65
|
+
display_name="Elevation",
|
66
|
+
unit="m",
|
67
|
+
format=".0f",
|
68
|
+
)
|
69
|
+
|
70
|
+
TIME_SERIES_COLUMNS = [column_speed, column_elevation]
|