geo-activity-playground 0.39.0__py3-none-any.whl → 0.40.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 (28) hide show
  1. geo_activity_playground/__init__.py +0 -0
  2. geo_activity_playground/alembic/versions/93cc82ad1b60_add_parametricplotspec.py +39 -0
  3. geo_activity_playground/core/__init__.py +0 -0
  4. geo_activity_playground/core/activities.py +3 -0
  5. geo_activity_playground/core/datamodel.py +55 -8
  6. geo_activity_playground/core/enrichment.py +7 -5
  7. geo_activity_playground/core/parametric_plot.py +108 -0
  8. geo_activity_playground/core/test_datamodel.py +7 -0
  9. geo_activity_playground/explorer/__init__.py +0 -0
  10. geo_activity_playground/importers/__init__.py +0 -0
  11. geo_activity_playground/importers/activity_parsers.py +11 -11
  12. geo_activity_playground/importers/strava_checkout.py +7 -0
  13. geo_activity_playground/webui/__init__.py +0 -0
  14. geo_activity_playground/webui/app.py +11 -3
  15. geo_activity_playground/webui/blueprints/__init__.py +0 -0
  16. geo_activity_playground/webui/blueprints/activity_blueprint.py +7 -7
  17. geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +89 -0
  18. geo_activity_playground/webui/blueprints/summary_blueprint.py +8 -0
  19. geo_activity_playground/webui/templates/activity/show.html.j2 +3 -3
  20. geo_activity_playground/webui/templates/page.html.j2 +5 -1
  21. geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +63 -0
  22. geo_activity_playground/webui/templates/plot_builder/index.html.j2 +32 -0
  23. geo_activity_playground/webui/templates/summary/index.html.j2 +12 -0
  24. {geo_activity_playground-0.39.0.dist-info → geo_activity_playground-0.40.0.dist-info}/METADATA +1 -1
  25. {geo_activity_playground-0.39.0.dist-info → geo_activity_playground-0.40.0.dist-info}/RECORD +28 -16
  26. {geo_activity_playground-0.39.0.dist-info → geo_activity_playground-0.40.0.dist-info}/LICENSE +0 -0
  27. {geo_activity_playground-0.39.0.dist-info → geo_activity_playground-0.40.0.dist-info}/WHEEL +0 -0
  28. {geo_activity_playground-0.39.0.dist-info → geo_activity_playground-0.40.0.dist-info}/entry_points.txt +0 -0
File without changes
@@ -0,0 +1,39 @@
1
+ from typing import Sequence
2
+ from typing import Union
3
+
4
+ import sqlalchemy as sa
5
+ from alembic import op
6
+
7
+
8
+ # revision identifiers, used by Alembic.
9
+ revision: str = "93cc82ad1b60"
10
+ down_revision: Union[str, None] = "e02e27876deb"
11
+ branch_labels: Union[str, Sequence[str], None] = None
12
+ depends_on: Union[str, Sequence[str], None] = None
13
+
14
+
15
+ def upgrade() -> None:
16
+ # ### commands auto generated by Alembic - please adjust! ###
17
+ op.create_table(
18
+ "plot_specs",
19
+ sa.Column("id", sa.Integer(), nullable=False),
20
+ sa.Column("name", sa.String(), nullable=False),
21
+ sa.Column("mark", sa.String(), nullable=True),
22
+ sa.Column("x", sa.String(), nullable=True),
23
+ sa.Column("y", sa.String(), nullable=True),
24
+ sa.Column("color", sa.String(), nullable=True),
25
+ sa.Column("shape", sa.String(), nullable=True),
26
+ sa.Column("size", sa.String(), nullable=True),
27
+ sa.Column("row", sa.String(), nullable=True),
28
+ sa.Column("opacity", sa.String(), nullable=True),
29
+ sa.Column("column", sa.String(), nullable=True),
30
+ sa.Column("facet", sa.String(), nullable=True),
31
+ sa.PrimaryKeyConstraint("id"),
32
+ )
33
+ # ### end Alembic commands ###
34
+
35
+
36
+ def downgrade() -> None:
37
+ # ### commands auto generated by Alembic - please adjust! ###
38
+ op.drop_table("plot_specs")
39
+ # ### end Alembic commands ###
File without changes
@@ -91,6 +91,9 @@ class ActivityRepository:
91
91
  df["hours"] = [
92
92
  elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
93
93
  ]
94
+ df["hours_moving"] = [
95
+ moving_time.total_seconds() / 3600 for moving_time in df["moving_time"]
96
+ ]
94
97
  df.index = df["id"]
95
98
  return df
96
99
 
@@ -1,6 +1,8 @@
1
1
  import datetime
2
+ import json
2
3
  import logging
3
4
  from typing import Any
5
+ from typing import Optional
4
6
  from typing import TypedDict
5
7
 
6
8
  import numpy as np
@@ -109,21 +111,25 @@ class Activity(DB.Model):
109
111
  return f"{self.start} {self.name}"
110
112
 
111
113
  @property
112
- def average_speed_moving_kmh(self) -> float:
113
- return self.distance_km / (self.moving_time.total_seconds() / 3_600)
114
+ def average_speed_moving_kmh(self) -> Optional[float]:
115
+ if self.moving_time is not None:
116
+ return self.distance_km / (self.moving_time.total_seconds() / 3_600)
114
117
 
115
118
  @property
116
- def average_speed_elapsed_kmh(self) -> float:
117
- return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
119
+ def average_speed_elapsed_kmh(self) -> Optional[float]:
120
+ if self.elapsed_time is not None:
121
+ return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
118
122
 
119
123
  @property
120
124
  def raw_time_series(self) -> pd.DataFrame:
121
125
  path = time_series_dir() / f"{self.id}.parquet"
122
126
  try:
123
- return pd.read_parquet(path)
127
+ time_series = pd.read_parquet(path)
128
+ if "altitude" in time_series.columns:
129
+ time_series.rename(columns={"altitude": "elevation"}, inplace=True)
130
+ return time_series
124
131
  except OSError as e:
125
- logger.error(f"Error while reading {path}, deleting cache file …")
126
- path.unlink(missing_ok=True)
132
+ logger.error(f"Error while reading {path}.")
127
133
  raise
128
134
 
129
135
  @property
@@ -234,7 +240,7 @@ def get_or_make_kind(name: str, config: Config) -> Kind:
234
240
  else:
235
241
  kind = Kind(
236
242
  name=name,
237
- consider_for_achievements=config.kinds_without_achievements.get(name, True),
243
+ consider_for_achievements=name in config.kinds_without_achievements,
238
244
  )
239
245
  DB.session.add(kind)
240
246
  return kind
@@ -255,3 +261,44 @@ def get_or_make_equipment(name: str, config: Config) -> Equipment:
255
261
  )
256
262
  DB.session.add(equipment)
257
263
  return equipment
264
+
265
+
266
+ class PlotSpec(DB.Model):
267
+ __tablename__ = "plot_specs"
268
+
269
+ id: Mapped[int] = mapped_column(primary_key=True)
270
+
271
+ name: Mapped[str] = mapped_column(sa.String, nullable=False)
272
+
273
+ mark: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
274
+ x: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
275
+ y: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
276
+ color: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
277
+ shape: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
278
+ size: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
279
+ row: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
280
+ opacity: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
281
+ column: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
282
+ facet: Mapped[str] = mapped_column(sa.String, nullable=False, default="")
283
+
284
+ FIELDS = [
285
+ "name",
286
+ "mark",
287
+ "x",
288
+ "y",
289
+ "color",
290
+ "shape",
291
+ "size",
292
+ "row",
293
+ "opacity",
294
+ "column",
295
+ "facet",
296
+ ]
297
+
298
+ def __str__(self) -> str:
299
+ return self.name
300
+
301
+ def to_json(self) -> str:
302
+ return json.dumps(
303
+ {key: getattr(self, key) for key in self.FIELDS if getattr(self, key)}
304
+ )
@@ -192,10 +192,12 @@ def _embellish_single_time_series(
192
192
  timeseries["y"] = y
193
193
 
194
194
  if "altitude" in timeseries.columns:
195
- altitude_diff = timeseries["altitude"].diff()
196
- altitude_diff = altitude_diff.ewm(span=5, min_periods=5).mean()
197
- altitude_diff.loc[altitude_diff.abs() > 30] = 0
198
- altitude_diff.loc[altitude_diff < 0] = 0
199
- timeseries["elevation_gain_cum"] = altitude_diff.cumsum()
195
+ timeseries.rename(columns={"altitude": "elevation"}, inplace=True)
196
+ if "elevation" in timeseries.columns:
197
+ elevation_diff = timeseries["elevation"].diff()
198
+ elevation_diff = elevation_diff.ewm(span=5, min_periods=5).mean()
199
+ elevation_diff.loc[elevation_diff.abs() > 30] = 0
200
+ elevation_diff.loc[elevation_diff < 0] = 0
201
+ timeseries["elevation_gain_cum"] = elevation_diff.cumsum()
200
202
 
201
203
  return timeseries
@@ -0,0 +1,108 @@
1
+ import altair as alt
2
+ import pandas as pd
3
+
4
+ from .datamodel import PlotSpec
5
+
6
+
7
+ MARKS = {
8
+ "point": "Point",
9
+ "circle": "Circle",
10
+ "area": "Area",
11
+ "bar": "Bar",
12
+ "rect": "Rectangle",
13
+ }
14
+ CONTINUOUS_VARIABLES = {
15
+ "distance_km": "Distance / km",
16
+ "sum(distance_km)": "Total distance / km",
17
+ "mean(distance_km)": "Average distance / km",
18
+ "start": "Date",
19
+ "hours": "Elapsed time / h",
20
+ "hours_moving": "Moving time / h",
21
+ "calories": "Energy / kcal",
22
+ "steps": "Steps",
23
+ "elevation_gain": "Elevation gain / m",
24
+ "start_elevation": "Start elevation / m",
25
+ "end_elevation": "End elevation / m",
26
+ "sum(elevation_gain)": "Total elevation gain / m",
27
+ "mean(elevation_gain)": "Average elevation gain / m",
28
+ "num_new_tiles_14": "New tiles 14",
29
+ "num_new_tiles_14": "New tiles 17",
30
+ "average_speed_moving_kmh": "Average moving speed / km/h",
31
+ "average_speed_elapsed_kmh": "Average elapsed speed / km/h",
32
+ "start_latitude": "Start latitude / °",
33
+ "start_longitude": "Start longitude / °",
34
+ "end_latitude": "End latitude / °",
35
+ "end_longitude": "End longitude / °",
36
+ }
37
+ DISCRETE_VARIABLES = {
38
+ "equipment": "Equipment",
39
+ "kind": "Activity kind",
40
+ "consider_for_achievements": "Consider for achievements",
41
+ "year(start):O": "Year",
42
+ "iso_year:O": "ISO Year",
43
+ "yearquarter(start)": "Year, Quarter",
44
+ "yearquartermonth(start)": "Year, Quarter, Month",
45
+ "yearmonth(start)": "Year, Month",
46
+ "quarter(start)": "Quarter",
47
+ "quartermonth(start)": "Quarter, Month",
48
+ "month(start)": "Month",
49
+ "week:O": "ISO Week",
50
+ "date(start)": "Day of month",
51
+ "weekday(start)": "Day of week",
52
+ }
53
+
54
+ VARIABLES_1 = {"": "", **DISCRETE_VARIABLES}
55
+ VARIABLES_2 = {"": "", **DISCRETE_VARIABLES, **CONTINUOUS_VARIABLES}
56
+
57
+
58
+ def make_parametric_plot(df: pd.DataFrame, spec: PlotSpec) -> str:
59
+ chart = alt.Chart(df)
60
+
61
+ match spec.mark:
62
+ case "point":
63
+ chart = chart.mark_point()
64
+ case "circle":
65
+ chart = chart.mark_circle()
66
+ case "area":
67
+ chart = chart.mark_area()
68
+ case "bar":
69
+ chart = chart.mark_bar()
70
+ case "rect":
71
+ chart = chart.mark_rect()
72
+ case _:
73
+ raise ValueError()
74
+
75
+ encodings = [
76
+ alt.X(spec.x, title=VARIABLES_2[spec.x]),
77
+ alt.Y(spec.y, title=VARIABLES_2[spec.y]),
78
+ ]
79
+ tooltips = [
80
+ alt.Tooltip(spec.x, title=VARIABLES_2[spec.x]),
81
+ alt.Tooltip(spec.y, title=VARIABLES_2[spec.y]),
82
+ ]
83
+
84
+ if spec.color:
85
+ encodings.append(alt.Color(spec.color, title=VARIABLES_2[spec.color]))
86
+ tooltips.append(alt.Tooltip(spec.color, title=VARIABLES_2[spec.color]))
87
+ if spec.shape:
88
+ encodings.append(alt.Shape(spec.shape, title=VARIABLES_2[spec.shape]))
89
+ tooltips.append(alt.Tooltip(spec.shape, title=VARIABLES_2[spec.shape]))
90
+ if spec.size:
91
+ encodings.append(alt.Size(spec.size, title=VARIABLES_2[spec.size]))
92
+ tooltips.append(alt.Tooltip(spec.size, title=VARIABLES_2[spec.size]))
93
+ if spec.opacity:
94
+ encodings.append(alt.Size(spec.opacity, title=VARIABLES_2[spec.opacity]))
95
+ tooltips.append(alt.Opacity(spec.opacity, title=VARIABLES_2[spec.opacity]))
96
+ if spec.row:
97
+ encodings.append(alt.Row(spec.row, title=VARIABLES_2[spec.row]))
98
+ tooltips.append(alt.Tooltip(spec.row, title=VARIABLES_2[spec.row]))
99
+ if spec.column:
100
+ encodings.append(alt.Column(spec.column, title=VARIABLES_2[spec.column]))
101
+ tooltips.append(alt.Tooltip(spec.column, title=VARIABLES_2[spec.column]))
102
+ if spec.facet:
103
+ encodings.append(
104
+ alt.Facet(spec.facet, columns=3, title=VARIABLES_2[spec.facet])
105
+ )
106
+ tooltips.append(alt.Tooltip(spec.facet, title=VARIABLES_2[spec.facet]))
107
+
108
+ return chart.encode(*encodings, tooltips).interactive().to_json(format="vega")
@@ -0,0 +1,7 @@
1
+ from .datamodel import Activity
2
+
3
+
4
+ def test_zero_duration() -> None:
5
+ activity = Activity(name="Test", distance_km=10.0)
6
+ assert activity.average_speed_elapsed_kmh is None
7
+ assert activity.average_speed_moving_kmh is None
File without changes
File without changes
@@ -121,9 +121,9 @@ def read_fit_activity(path: pathlib.Path, open) -> tuple[ActivityMeta, pd.DataFr
121
121
  if "distance" in fields:
122
122
  row["distance"] = values["distance"]
123
123
  if "altitude" in fields:
124
- row["altitude"] = values["altitude"]
124
+ row["elevation"] = values["altitude"]
125
125
  if "enhanced_altitude" in fields:
126
- row["altitude"] = values["enhanced_altitude"]
126
+ row["elevation"] = values["enhanced_altitude"]
127
127
  if "speed" in fields:
128
128
  factor = _fit_speed_unit_factor(fields["speed"].units)
129
129
  row["speed"] = values["speed"] * factor
@@ -188,10 +188,10 @@ def read_gpx_activity(path: pathlib.Path, open) -> pd.DataFrame:
188
188
  time = convert_to_datetime_ns(time)
189
189
  points.append((time, point.latitude, point.longitude, point.elevation))
190
190
 
191
- df = pd.DataFrame(points, columns=["time", "latitude", "longitude", "altitude"])
192
- # Some files don't have altitude information. In these cases we remove the column.
193
- if not df["altitude"].any():
194
- del df["altitude"]
191
+ df = pd.DataFrame(points, columns=["time", "latitude", "longitude", "elevation"])
192
+ # Some files don't have elevation information. In these cases we remove the column.
193
+ if not df["elevation"].any():
194
+ del df["elevation"]
195
195
  return df
196
196
 
197
197
 
@@ -230,7 +230,7 @@ def read_tcx_activity(path: pathlib.Path, opener) -> pd.DataFrame:
230
230
  "longitude": trackpoint.longitude,
231
231
  }
232
232
  if trackpoint.elevation:
233
- row["altitude"] = trackpoint.elevation
233
+ row["elevation"] = trackpoint.elevation
234
234
  if trackpoint.hr_value:
235
235
  row["heartrate"] = trackpoint.hr_value
236
236
  if trackpoint.cadence:
@@ -256,16 +256,16 @@ def read_kml_activity(path: pathlib.Path, opener) -> pd.DataFrame:
256
256
  parts = where.split(" ")
257
257
  if len(parts) == 2:
258
258
  lon, lat = parts
259
- alt = None
259
+ elevation = None
260
260
  if len(parts) == 3:
261
- lon, lat, alt = parts
261
+ lon, lat, elevation = parts
262
262
  row = {
263
263
  "time": time,
264
264
  "latitude": float(lat),
265
265
  "longitude": float(lon),
266
266
  }
267
- if alt is not None:
268
- row["altitude"] = float(alt)
267
+ if elevation is not None:
268
+ row["elevation"] = float(elevation)
269
269
  rows.append(row)
270
270
  return pd.DataFrame(rows)
271
271
 
@@ -258,6 +258,13 @@ def convert_strava_checkout(
258
258
  activities = pd.read_csv(checkout_path / "activities.csv")
259
259
  print(activities)
260
260
 
261
+ # Handle German localization.
262
+ if activities.columns[0] == "Aktivitäts-ID":
263
+ assert len(activities.columns) == len(
264
+ EXPECTED_COLUMNS
265
+ ), "Strava seems to have changed for format again. Please file a bug report at https://github.com/martin-ueding/geo-activity-playground/issues and include the first line of the 'activities.csv'."
266
+ activities.columns = EXPECTED_COLUMNS
267
+
261
268
  for _, row in tqdm(activities.iterrows(), desc="Import activity files"):
262
269
  # Some people have manually added activities without position data. These don't have a file there. We'll skip these.
263
270
  if not isinstance(row["Filename"], str):
File without changes
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import importlib
3
3
  import json
4
+ import logging
4
5
  import os
5
6
  import pathlib
6
7
  import secrets
@@ -32,6 +33,7 @@ from .blueprints.entry_views import register_entry_views
32
33
  from .blueprints.equipment_blueprint import make_equipment_blueprint
33
34
  from .blueprints.explorer_blueprint import make_explorer_blueprint
34
35
  from .blueprints.heatmap_blueprint import make_heatmap_blueprint
36
+ from .blueprints.plot_builder_blueprint import make_plot_builder_blueprint
35
37
  from .blueprints.search_blueprint import make_search_blueprint
36
38
  from .blueprints.settings_blueprint import make_settings_blueprint
37
39
  from .blueprints.square_planner_blueprint import make_square_planner_blueprint
@@ -43,6 +45,9 @@ from .flasher import FlaskFlasher
43
45
  from .search_util import SearchQueryHistory
44
46
 
45
47
 
48
+ logger = logging.getLogger(__name__)
49
+
50
+
46
51
  def get_secret_key():
47
52
  secret_file = pathlib.Path("Cache/flask-secret.json")
48
53
  if secret_file.exists():
@@ -65,9 +70,9 @@ def web_ui_main(
65
70
 
66
71
  app = Flask(__name__)
67
72
 
68
- app.config["SQLALCHEMY_DATABASE_URI"] = (
69
- f"sqlite:///{basedir.absolute()}/database.sqlite"
70
- )
73
+ database_path = basedir / "database.sqlite"
74
+ logger.info(f"Using database file at '{database_path.absolute()}'.")
75
+ app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{database_path.absolute()}"
71
76
  app.config["ALEMBIC"] = {"script_location": "../alembic/versions"}
72
77
  DB.init_app(app)
73
78
 
@@ -137,6 +142,9 @@ def web_ui_main(
137
142
  "/heatmap": make_heatmap_blueprint(
138
143
  repository, tile_visit_accessor, config_accessor(), search_query_history
139
144
  ),
145
+ "/plot-builder": make_plot_builder_blueprint(
146
+ repository, flasher, authenticator
147
+ ),
140
148
  "/settings": make_settings_blueprint(config_accessor, authenticator, flasher),
141
149
  "/square-planner": make_square_planner_blueprint(tile_visit_accessor),
142
150
  "/search": make_search_blueprint(
File without changes
@@ -148,8 +148,8 @@ def make_activity_blueprint(
148
148
  )
149
149
  ) is not None:
150
150
  context["heart_zones_plot"] = heart_rate_zone_plot(heart_zones)
151
- if "altitude" in time_series.columns:
152
- context["altitude_time_plot"] = altitude_time_plot(time_series)
151
+ if "elevation" in time_series.columns:
152
+ context["elevation_time_plot"] = elevation_time_plot(time_series)
153
153
  if "elevation_gain_cum" in time_series.columns:
154
154
  context["elevation_gain_cum_plot"] = elevation_gain_cum_plot(time_series)
155
155
  if "heartrate" in time_series.columns:
@@ -445,13 +445,13 @@ def distance_time_plot(time_series: pd.DataFrame) -> str:
445
445
  )
446
446
 
447
447
 
448
- def altitude_time_plot(time_series: pd.DataFrame) -> str:
448
+ def elevation_time_plot(time_series: pd.DataFrame) -> str:
449
449
  return (
450
- alt.Chart(time_series, title="Altitude")
450
+ alt.Chart(time_series, title="Elevation")
451
451
  .mark_line()
452
452
  .encode(
453
453
  alt.X("time", title="Time"),
454
- alt.Y("altitude", scale=alt.Scale(zero=False), title="Altitude / m"),
454
+ alt.Y("elevation", scale=alt.Scale(zero=False), title="Elevation / m"),
455
455
  alt.Color("segment_id:N", title="Segment"),
456
456
  )
457
457
  .interactive(bind_y=False)
@@ -461,14 +461,14 @@ def altitude_time_plot(time_series: pd.DataFrame) -> str:
461
461
 
462
462
  def elevation_gain_cum_plot(time_series: pd.DataFrame) -> str:
463
463
  return (
464
- alt.Chart(time_series, title="Altitude Gain")
464
+ alt.Chart(time_series, title="Elevation Gain")
465
465
  .mark_line()
466
466
  .encode(
467
467
  alt.X("time", title="Time"),
468
468
  alt.Y(
469
469
  "elevation_gain_cum",
470
470
  scale=alt.Scale(zero=False),
471
- title="Altitude gain / m",
471
+ title="Elevation gain / m",
472
472
  ),
473
473
  alt.Color("segment_id:N", title="Segment"),
474
474
  )
@@ -0,0 +1,89 @@
1
+ import sqlalchemy
2
+ from flask import Blueprint
3
+ from flask import redirect
4
+ from flask import render_template
5
+ from flask import request
6
+ from flask import Response
7
+ from flask import url_for
8
+
9
+ from ...core.activities import ActivityRepository
10
+ from ...core.datamodel import DB
11
+ from ...core.parametric_plot import make_parametric_plot
12
+ from ...core.parametric_plot import MARKS
13
+ from ...core.parametric_plot import PlotSpec
14
+ from ...core.parametric_plot import VARIABLES_1
15
+ from ...core.parametric_plot import VARIABLES_2
16
+ from ..authenticator import Authenticator
17
+ from ..authenticator import needs_authentication
18
+ from ..flasher import Flasher
19
+ from ..flasher import FlashTypes
20
+
21
+
22
+ def make_plot_builder_blueprint(
23
+ repository: ActivityRepository, flasher: Flasher, authenticator: Authenticator
24
+ ) -> Blueprint:
25
+ blueprint = Blueprint("plot_builder", __name__, template_folder="templates")
26
+
27
+ @blueprint.route("/")
28
+ def index() -> Response:
29
+ return render_template(
30
+ "plot_builder/index.html.j2",
31
+ specs=DB.session.scalars(sqlalchemy.select(PlotSpec)).all(),
32
+ )
33
+
34
+ @blueprint.route("/new")
35
+ @needs_authentication(authenticator)
36
+ def new() -> Response:
37
+ spec = PlotSpec(
38
+ name="My New Plot",
39
+ mark="bar",
40
+ x="year(start):O",
41
+ y="sum(distance_km)",
42
+ color="kind",
43
+ )
44
+ DB.session.add(spec)
45
+ DB.session.commit()
46
+ return redirect(url_for(".edit", id=spec.id))
47
+
48
+ @blueprint.route("/edit/<int:id>")
49
+ @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"]
65
+ try:
66
+ plot = make_parametric_plot(repository.meta, spec)
67
+ DB.session.commit()
68
+ except ValueError as e:
69
+ plot = None
70
+ flasher.flash_message(str(e), FlashTypes.WARNING)
71
+ return render_template(
72
+ "plot_builder/edit.html.j2",
73
+ marks=MARKS,
74
+ discrete=VARIABLES_1,
75
+ continuous=VARIABLES_2,
76
+ plot=plot,
77
+ spec=spec,
78
+ )
79
+
80
+ @blueprint.route("/delete/<int:id>")
81
+ @needs_authentication(authenticator)
82
+ def delete(id: int) -> Response:
83
+ spec = DB.session.get(PlotSpec, id)
84
+ DB.session.delete(spec)
85
+ flasher.flash_message(f"Deleted plot '{spec.name}'.", FlashTypes.SUCCESS)
86
+ DB.session.commit()
87
+ return redirect(url_for(".index"))
88
+
89
+ return blueprint
@@ -3,6 +3,7 @@ import datetime
3
3
 
4
4
  import altair as alt
5
5
  import pandas as pd
6
+ import sqlalchemy
6
7
  from flask import Blueprint
7
8
  from flask import render_template
8
9
  from flask import request
@@ -10,7 +11,10 @@ from flask import request
10
11
  from ...core.activities import ActivityRepository
11
12
  from ...core.activities import make_geojson_from_time_series
12
13
  from ...core.config import Config
14
+ from ...core.datamodel import DB
15
+ from ...core.datamodel import PlotSpec
13
16
  from ...core.meta_search import apply_search_query
17
+ from ...core.parametric_plot import make_parametric_plot
14
18
  from ..plot_util import make_kind_scale
15
19
  from ..search_util import search_query_from_form
16
20
  from ..search_util import SearchQueryHistory
@@ -62,6 +66,10 @@ def make_summary_blueprint(
62
66
  for activity_id, reasons in nominations.items()
63
67
  ],
64
68
  query=query.to_jinja(),
69
+ custom_plots=[
70
+ (spec, make_parametric_plot(repository.meta, spec))
71
+ for spec in DB.session.scalars(sqlalchemy.select(PlotSpec)).all()
72
+ ],
65
73
  )
66
74
 
67
75
  return blueprint
@@ -132,16 +132,16 @@
132
132
  </div>
133
133
  </div>
134
134
 
135
- {% if altitude_time_plot is defined %}
135
+ {% if elevation_time_plot is defined %}
136
136
  <div class="row mb-3">
137
137
  <div class="col">
138
- <h2>Altitude</h2>
138
+ <h2>Elevation</h2>
139
139
  </div>
140
140
  </div>
141
141
 
142
142
  <div class="row mb-3">
143
143
  <div class="col-md-4">
144
- {{ vega_direct("altitude_time_plot", altitude_time_plot) }}
144
+ {{ vega_direct("elevation_time_plot", elevation_time_plot) }}
145
145
  </div>
146
146
  {% if elevation_gain_cum_plot is defined %}
147
147
  <div class="col-md-4">
@@ -130,6 +130,8 @@
130
130
  <hr class="dropdown-divider">
131
131
  </li>
132
132
 
133
+ <li><a class="dropdown-item" href="{{ url_for('plot_builder.index') }}">Plot Builder</a>
134
+ </li>
133
135
  <li><a class="dropdown-item" href="{{ url_for('settings.index') }}">Settings</a></li>
134
136
  </ul>
135
137
  </li>
@@ -196,7 +198,9 @@
196
198
 
197
199
  <div class="row border-top py-3 my-4">
198
200
  <ul class="nav col-4">
199
- <li class="nav-item px-2 nav-link"><a href="https://github.com/martin-ueding/geo-activity-playground/blob/main/docs/changelog.md" class="nav-link px-2 text-muted" target="_blank">Version {{ version }}</a></li>
201
+ <li class="nav-item px-2 nav-link"><a
202
+ href="https://github.com/martin-ueding/geo-activity-playground/blob/main/docs/changelog.md"
203
+ class="nav-link px-2 text-muted" target="_blank">Version {{ version }}</a></li>
200
204
  </ul>
201
205
  <ul class="nav col-8 justify-content-end">
202
206
  <li class="nav-item"><a href="https://github.com/martin-ueding/geo-activity-playground"
@@ -0,0 +1,63 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="row mb-3">Plot Builder</h1>
6
+
7
+ <div class="mb-3"><a class="btn btn-primary" href="{{ url_for('.index') }}">Back to overview</a></div>
8
+
9
+
10
+ <div class="row mb-3">
11
+ <div class="col-md-4">
12
+ {% macro select_field(label, name, choices, active) %}
13
+ <div class="mb-3">
14
+ <label for="{{ name }}">{{ label }}</label>
15
+ <select name="{{ name }}" class="form-select">
16
+ {% for choice_value, choice_label in choices.items() %}
17
+ <option id="{{ name }}" value="{{ choice_value }}" {% if active==choice_value %} selected {% endif %}>
18
+ {{ choice_label }}
19
+ </option>
20
+ {% endfor %}
21
+ </select>
22
+ </div>
23
+ {% endmacro %}
24
+
25
+ <form>
26
+ <div class="mb-3">
27
+ <label for="name">Name</label>
28
+ <input type="text" name="name" id="name" class="form-control" value="{{ spec.name }}">
29
+ </div>
30
+
31
+ {{ select_field("Mark", "mark", marks, spec.mark)}}
32
+
33
+ {{ select_field("X", "x", continuous, spec.x)}}
34
+ {{ select_field("Y", "y", continuous, spec.y)}}
35
+ {{ select_field("Color", "color", continuous, spec.color)}}
36
+ {{ select_field("Size", "size", continuous, spec.size)}}
37
+ {{ select_field("Shape", "shape", discrete, spec.shape)}}
38
+ {{ select_field("Opacity", "opacity", discrete, spec.opacity)}}
39
+ {{ select_field("Facet", "facet", discrete, spec.facet)}}
40
+ {{ select_field("Row", "row", discrete, spec.row)}}
41
+ {{ select_field("Column", "column", discrete, spec.column)}}
42
+
43
+ <button type="submit" class="btn btn-primary">Save & Preview</button>
44
+ </form>
45
+ </div>
46
+ <div class="col-md-8">
47
+ {% if plot %}
48
+ {{ vega_direct("plot", plot) }}
49
+ {% endif %}
50
+ </div>
51
+ </div>
52
+
53
+
54
+ <h1 class="mb-3">JSON Export</h1>
55
+
56
+ <p>If you want to share this plot specification with somebody else, send them the following code snippet. It contains
57
+ the name that you have given your plot but no data from your activities.</p>
58
+
59
+ <code><pre>{{ spec.to_json() }}</pre></code>
60
+
61
+
62
+
63
+ {% endblock %}
@@ -0,0 +1,32 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="row mb-3">Plot Builder</h1>
6
+
7
+ {% if specs %}
8
+ <table class="table mb-3">
9
+ <thead>
10
+ <tr>
11
+ <th>Name</th>
12
+ <th>Actions</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ {% for spec in specs %}
17
+ <tr>
18
+ <td>{{ spec.name }}</td>
19
+ <td>
20
+ <a class="btn btn-small btn-primary" href="{{ url_for('.edit', id=spec.id) }}">Edit</a>
21
+ <a class="btn btn-small btn-danger" href="{{ url_for('.delete', id=spec.id) }}"
22
+ onclick="if(!confirm('Are you sure to Delete This?')){ event.preventDefault() }">Delete</a>
23
+ </td>
24
+ </tr>
25
+ {% endfor %}
26
+ </tbody>
27
+ </table>
28
+ {% endif %}
29
+
30
+ <a class="btn btn-primary" href="{{ url_for('.new') }}">New</a>
31
+
32
+ {% endblock %}
@@ -9,6 +9,18 @@
9
9
  {{ search_form(query, equipments_avail, kinds_avail, search_query_favorites, search_query_last, request_url) }}
10
10
  </div>
11
11
 
12
+ {% if custom_plots %}
13
+ <h2>Your custom plots</h2>
14
+ {% for spec, plot in custom_plots %}
15
+ <h3>{{ spec.name }}</h3>
16
+ <div class="row mb-3">
17
+ <div class="col">
18
+ {{ vega_direct("custom_plot_" ~ loop.index, plot) }}
19
+ </div>
20
+ </div>
21
+ {% endfor %}
22
+ {% endif %}
23
+
12
24
  <h2>Distances</h2>
13
25
 
14
26
  <p>This is your weekly distance for the past rolling year, split by activity kind.</p>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -1,47 +1,56 @@
1
+ geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1
2
  geo_activity_playground/__main__.py,sha256=eL7NlKydYrzi4ikTvvKmlwkEFxx0V6CXOHOhRtazmd8,2907
2
3
  geo_activity_playground/alembic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
3
4
  geo_activity_playground/alembic/env.py,sha256=46oMzwSROaAsYuYWTd46txFdRLD3adm_SCn01A_ex8Q,2081
4
5
  geo_activity_playground/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
5
6
  geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py,sha256=WrmlDllnJECg6cSOeS05wYCa977_SXbJUV5khDSzntw,1082
6
7
  geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py,sha256=YTnnENkQ8WqLz7PFof7tUWNkWcoHGkAfAM52x1N9umo,3029
8
+ geo_activity_playground/alembic/versions/93cc82ad1b60_add_parametricplotspec.py,sha256=P9nG348kz6Wi2lD8lJ_f-0juBtlJb1tqBWYqP2XMQPE,1365
7
9
  geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py,sha256=Wz02lBP2r7-09DjuQP8u8i7ypQ2SZU5RUc422-_ZBDk,851
8
10
  geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py,sha256=1pt7aes0PWJXZ98HxqeDK-ehaU9KLApjCmZYoqCa8V0,975
9
11
  geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py,sha256=Y0OMxp5z_-CQ83rww6GEBFRawXu0J0pLrLArgSjJ7wQ,866
10
12
  geo_activity_playground/alembic/versions/script.py.mako,sha256=3qBrHBf7F7ChKDUIdiNItiSXrDpgQdM7sR0YKzpaC50,689
11
- geo_activity_playground/core/activities.py,sha256=7Z8xiT0juLdo1nQ3fPqWKtSp6MjRkSzd-_FYrzb-Tpg,5360
13
+ geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ geo_activity_playground/core/activities.py,sha256=R3RDvOkHyWL2Wr-Bgqjgr5wDjVqaJ3WSECL2fbMyiu8,5485
12
15
  geo_activity_playground/core/config.py,sha256=eGWWbNfHa6H64AHCnFYTsAJ7-pWi-PhyxL4hjZ4u03U,5256
13
16
  geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
14
- geo_activity_playground/core/datamodel.py,sha256=cbVNvvHLYHYBg-Gaabm3bewfro8uQ4Sqdq_0mujcos0,8693
15
- geo_activity_playground/core/enrichment.py,sha256=LWAKF6mAKtiZcIF8ptAgpt8fYTYZAnSuXGHuX5TTO6o,7220
17
+ geo_activity_playground/core/datamodel.py,sha256=v5tLkPO53ilVIE78_gLz7igi6qh0ffdDPJmF7P2lkgE,10303
18
+ geo_activity_playground/core/enrichment.py,sha256=kc9747ocSs2_3R7oW9Rjs3_lKP37gdvBUbyWpILaqHc,7346
16
19
  geo_activity_playground/core/heart_rate.py,sha256=-S3WAhS7AOywrw_Lk5jfuo_fu6zvZQ1VtjwEKSycWpU,1542
17
20
  geo_activity_playground/core/meta_search.py,sha256=naErjAC7ZCFhOF6d492kbegZxCdzbpGcJvjQLJTE4xE,5016
21
+ geo_activity_playground/core/parametric_plot.py,sha256=IefPc6lwthxowvjUDA5wu23oBSw9jq399l04gSaNrOQ,3880
18
22
  geo_activity_playground/core/paths.py,sha256=aUXGuNn9hBvGPQWPoUJeImHN0PB0fS1tja1tm2eq8mA,2595
19
23
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
20
24
  geo_activity_playground/core/raster_map.py,sha256=Cq8dNLdxVQg3Agzn2bmXVu0-8kZf56QrSe-LKNn3jaU,7994
21
25
  geo_activity_playground/core/similarity.py,sha256=L2de3DPRdDeDY5AxZwLDcH7FjHWRWklr41VNU06q9kQ,3117
22
26
  geo_activity_playground/core/summary_stats.py,sha256=v5FtWnE1imDF5axI6asVN55wCrlD73oZ6lvqzxsTN2c,1006
23
27
  geo_activity_playground/core/tasks.py,sha256=-_9cxekoHSWzCW4XblNeqrwi2tTqr5AE7_-p8fdqhwc,2886
28
+ geo_activity_playground/core/test_datamodel.py,sha256=noNgiGzs2BOZvdi2FosLvW8YTinE6Gl7_JrEs7D8GBI,230
24
29
  geo_activity_playground/core/test_meta_search.py,sha256=zhuD343Xce-4Fkznw81DHQ7pK5eyX5UbcyCHuYRKsr8,3091
25
30
  geo_activity_playground/core/test_summary_stats.py,sha256=qH_45mPRFD2H-Rr0Ku-RYc67vhC7qKxbPr7J2F36uV8,3081
26
31
  geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
27
32
  geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
28
33
  geo_activity_playground/core/tiles.py,sha256=lV6X1Uc9XQecu2LALIvxpnMcLsVtWx7JczJ5a_S1eZE,2139
29
34
  geo_activity_playground/core/time_conversion.py,sha256=x5mXG6Y4GtdX7CBmwucGNSWBp9JQJDbZ7u0JkdUY1Vs,379
35
+ geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
36
  geo_activity_playground/explorer/grid_file.py,sha256=YNL_c4O1-kxaajATJwj4ZLywCL5Hpj9qy2h-F7rk8Yg,3260
31
37
  geo_activity_playground/explorer/tile_visits.py,sha256=C8IpAGmrjMGYhyTVK-tl2ptM9-CXF2mwibhJYn7gLf8,13905
32
38
  geo_activity_playground/explorer/video.py,sha256=7j6Qv3HG6On7Tn7xh7Olwrx_fbQnfzS7CeRg3TEApHg,4397
33
39
  geo_activity_playground/heatmap_video.py,sha256=I8i1uVvbbPUXVtvLAROaLy58nQoUPnuMCZkERWNkQjg,3318
34
- geo_activity_playground/importers/activity_parsers.py,sha256=DL11K2KFcESo7SC4CrvV4u1RALT5TbUJ22oOp7f1aG0,11058
40
+ geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ geo_activity_playground/importers/activity_parsers.py,sha256=yD7L5eDOpiLWf6RHSQf4-Nk2S3vgfVHngc9ZlFSrioM,11090
35
42
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
36
43
  geo_activity_playground/importers/directory.py,sha256=4Q7UAFa7ztkgqf4FvPbH2LlrO-7a8Fu7tkYPHOpHm1g,5210
37
44
  geo_activity_playground/importers/strava_api.py,sha256=J0-VXNrLq22fhTcWkQPE5AVrzy5aegC7SBi-UXFtAy4,7576
38
- geo_activity_playground/importers/strava_checkout.py,sha256=EixNFXXnxkopqUs0qe6iShYTbGu_o_g2_1lxuUIsz4E,9679
45
+ geo_activity_playground/importers/strava_checkout.py,sha256=uNcmA-ADURNpW5zJdRkG07eHjj1MVoIKsv0EWdc5iRw,10090
39
46
  geo_activity_playground/importers/test_csv_parser.py,sha256=nOTVTdlzIY0TDcbWp7xNyNaIO6Mkeu55hVziVl22QE4,1092
40
47
  geo_activity_playground/importers/test_directory.py,sha256=_fn_-y98ZyElbG0BRxAmGFdtGobUShPU86SdEOpuv-A,691
41
48
  geo_activity_playground/importers/test_strava_api.py,sha256=7b8bl5Rh2BctCmvTPEhCadxtUOq3mfzuadD6F5XxRio,398
42
- geo_activity_playground/webui/app.py,sha256=K-xZsNjorxPhVl9_z-W1MX0_s3gTjZ8XU7P56SFhwDk,6872
49
+ geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ geo_activity_playground/webui/app.py,sha256=xoRnF2ISq74v48ZBpyrCxEFPCwtS4fw3WZvWbvExSmE,7208
43
51
  geo_activity_playground/webui/authenticator.py,sha256=jtQqvpVHa_eLTAulmvvJgDRoCWOEege49G9zn3MfYk8,1394
44
- geo_activity_playground/webui/blueprints/activity_blueprint.py,sha256=ERYcUqvaG-hQqOr6uKEIZzGk1WelNKDHwvecsfu2c3c,24328
52
+ geo_activity_playground/webui/blueprints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ geo_activity_playground/webui/blueprints/activity_blueprint.py,sha256=A0hxhdEY_Ma5AEWT3Gv70Ys70DWSqSJOEdQAMeNb5CU,24337
45
54
  geo_activity_playground/webui/blueprints/auth_blueprint.py,sha256=_VZeP3VN626BoOOZUkNVnuw9v-cEOrkHz5lhFPmxqMY,784
46
55
  geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py,sha256=tiqDf-DZLdjm8faatZ5fqbh7gkII2KmBPRtkcKqoLIA,2194
47
56
  geo_activity_playground/webui/blueprints/calendar_blueprint.py,sha256=L6R8xieYYXeEMDzJs-MjWax1JAHhppWy3r3U8MyCOAk,2585
@@ -50,10 +59,11 @@ geo_activity_playground/webui/blueprints/entry_views.py,sha256=-CRrE5b9QYyrmQhLY
50
59
  geo_activity_playground/webui/blueprints/equipment_blueprint.py,sha256=juQ5L2BlrECb00LBbiY2yc0b8W_B9Y3fPwtbiaRfgpo,5634
51
60
  geo_activity_playground/webui/blueprints/explorer_blueprint.py,sha256=ZNDRwtmYvWGyqDe3fszGp0DR5qzRRxRss32ZGPRuXsY,12469
52
61
  geo_activity_playground/webui/blueprints/heatmap_blueprint.py,sha256=iHI5YJYhX7ZOlzTgzl2efIRDzt3UMYCx7X4-LVd0MWk,8702
62
+ geo_activity_playground/webui/blueprints/plot_builder_blueprint.py,sha256=7HrjpBM-608HSOh0i31Lmt7yDNMfWlEn6G7DlYqlV9w,3031
53
63
  geo_activity_playground/webui/blueprints/search_blueprint.py,sha256=Sv_KL1Cdai26y51qVfI-5jZLhtElREsEar1dbR_VAC4,2275
54
64
  geo_activity_playground/webui/blueprints/settings_blueprint.py,sha256=UUv63BDQFnBPq8fLDdlWHd5mxL5qIgcGUuqQRFemyEA,16108
55
65
  geo_activity_playground/webui/blueprints/square_planner_blueprint.py,sha256=xVaxJxmt8Dysl3UL9f2y__LVLtTH2Np1Ust4OSXKRAk,4746
56
- geo_activity_playground/webui/blueprints/summary_blueprint.py,sha256=g7j0wmqk3PwPJ-zQrSEJMcke1SUsrSkiOBhOfzaIP1A,9126
66
+ geo_activity_playground/webui/blueprints/summary_blueprint.py,sha256=-CArocRkcjzC7aTOCsG_8BnLJSX2b9X9C79mesyfvRw,9465
57
67
  geo_activity_playground/webui/blueprints/tile_blueprint.py,sha256=YzZf9OrNdjhc1_j4MtO1DMcw1uCv29ueNsYd-mWqgbg,837
58
68
  geo_activity_playground/webui/blueprints/upload_blueprint.py,sha256=_VeGu08vlRZlRn5J4t7VdBk2TTW5GXB4JUcge9mbX9Y,4111
59
69
  geo_activity_playground/webui/flasher.py,sha256=Covc1D9cO_jjokRWnvyiXCc2tfp3aZ8XkNqFdA1AXtk,500
@@ -97,7 +107,7 @@ geo_activity_playground/webui/templates/activity/day.html.j2,sha256=CHEvxlZralCm
97
107
  geo_activity_playground/webui/templates/activity/edit.html.j2,sha256=9HDFjYfUQBB6HAgeIZppFPlpiJ1vDZWcGyP7uYG_Hnw,1369
98
108
  geo_activity_playground/webui/templates/activity/lines.html.j2,sha256=_ZDg1ruW-9UMJfOudy1-uY_-IcSSaagq7tPCih5Bb8g,1079
99
109
  geo_activity_playground/webui/templates/activity/name.html.j2,sha256=7Wbh3IrVL5lMRve467H0P10Shn5FzGpaXLhV0H-X4Hk,2725
100
- geo_activity_playground/webui/templates/activity/show.html.j2,sha256=OjgD8uM1L8nO12yPcHAhqMPFOtA2k2iUNkCT3EhHBg8,8318
110
+ geo_activity_playground/webui/templates/activity/show.html.j2,sha256=NKXplqDMkzS5z7JKJliNr5bKxTG6cr3ahd6vrmN0280,8322
101
111
  geo_activity_playground/webui/templates/activity/trim.html.j2,sha256=3oAXQab6QqWjGBC9KCvWNOVn8uRmxoDLj3hx_O63TXc,1836
102
112
  geo_activity_playground/webui/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
103
113
  geo_activity_playground/webui/templates/bubble_chart/index.html.j2,sha256=pqyafhIgL97FuD4_-lMb8lRWC3rejwrjawbmfp17XFY,1143
@@ -108,7 +118,9 @@ geo_activity_playground/webui/templates/equipment/index.html.j2,sha256=wwrGmfCCB
108
118
  geo_activity_playground/webui/templates/explorer/index.html.j2,sha256=3t9ikAF6oMvEaVlS3Kb1tj9ngomIQlatzqPnqVsEDKA,6908
109
119
  geo_activity_playground/webui/templates/heatmap/index.html.j2,sha256=kTVvEt-GmSNebDlVMa6zwyIuP0mJcZQFuqj-IY8JV5U,1359
110
120
  geo_activity_playground/webui/templates/home.html.j2,sha256=vQp9uMn7BLY7pexWJVpQVWN8ZbbtWZvkW_hYSkYQeZs,2212
111
- geo_activity_playground/webui/templates/page.html.j2,sha256=TSNEZNLFzJ76G_22dDIXoTozfdWjmKk03qZl6exsdCQ,11043
121
+ geo_activity_playground/webui/templates/page.html.j2,sha256=1if_h8w2nsq9wNut-nzlpTNsPsQTxV0yPMdgUvz7bz8,11250
122
+ geo_activity_playground/webui/templates/plot_builder/edit.html.j2,sha256=x5Ki425me3HY6CcBQ37le9g8rCpbOxFVkdr0N_L84-g,2230
123
+ geo_activity_playground/webui/templates/plot_builder/index.html.j2,sha256=fBuGLT2HIwlgz5eGeKXOdIDqzDSQoY99w-hyt_0JP-w,832
112
124
  geo_activity_playground/webui/templates/search/index.html.j2,sha256=_kxTgsdbT8o-4ryW0pvyWE7a-rOs7xzGUpdSPp8Q1is,1320
113
125
  geo_activity_playground/webui/templates/search_form.html.j2,sha256=TG9xIql0HnhsXtbHZxl3GLBt6cGYjA8jPeBq11syQ3A,6512
114
126
  geo_activity_playground/webui/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
@@ -123,11 +135,11 @@ geo_activity_playground/webui/templates/settings/segmentation.html.j2,sha256=QV7
123
135
  geo_activity_playground/webui/templates/settings/sharepic.html.j2,sha256=qZkfEpd4CtKKMaSSVadqvNEgMRYLV-0X-pw5-nJvukk,678
124
136
  geo_activity_playground/webui/templates/settings/strava.html.j2,sha256=GCE5gskQ6xJ8AM1qGrrUVLDOiuqg510mWzzsZjia0gk,2211
125
137
  geo_activity_playground/webui/templates/square_planner/index.html.j2,sha256=-OnY2nQCgZCslOzf28ogZwFykwF8tZm7PgFwOE3eBDk,8176
126
- geo_activity_playground/webui/templates/summary/index.html.j2,sha256=bM89LdumssVvmuI0EeI8mnzMYxSzNjWY_XWfzn-f6nI,5377
138
+ geo_activity_playground/webui/templates/summary/index.html.j2,sha256=T8YUGMXaZYeVl7Q5-H1YhdnGo3bx_LtExPQ60qj_Zhs,5638
127
139
  geo_activity_playground/webui/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
128
140
  geo_activity_playground/webui/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
129
- geo_activity_playground-0.39.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
130
- geo_activity_playground-0.39.0.dist-info/METADATA,sha256=jughq9g1K5BCZKW7diQ6A8p9Lmp1CJk3vlQhDUcYmc0,1758
131
- geo_activity_playground-0.39.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
132
- geo_activity_playground-0.39.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
133
- geo_activity_playground-0.39.0.dist-info/RECORD,,
141
+ geo_activity_playground-0.40.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
142
+ geo_activity_playground-0.40.0.dist-info/METADATA,sha256=lhX789KVp1udKHGEsa3XojbAwFYRHs2X1E0DlTr9veo,1758
143
+ geo_activity_playground-0.40.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
144
+ geo_activity_playground-0.40.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
145
+ geo_activity_playground-0.40.0.dist-info/RECORD,,