geo-activity-playground 0.39.1__py3-none-any.whl → 0.40.1__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 (27) 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/datamodel.py +54 -7
  5. geo_activity_playground/core/enrichment.py +7 -5
  6. geo_activity_playground/core/parametric_plot.py +53 -46
  7. geo_activity_playground/core/test_datamodel.py +20 -0
  8. geo_activity_playground/explorer/__init__.py +0 -0
  9. geo_activity_playground/importers/__init__.py +0 -0
  10. geo_activity_playground/importers/activity_parsers.py +11 -11
  11. geo_activity_playground/importers/strava_checkout.py +9 -5
  12. geo_activity_playground/webui/__init__.py +0 -0
  13. geo_activity_playground/webui/app.py +10 -4
  14. geo_activity_playground/webui/blueprints/__init__.py +0 -0
  15. geo_activity_playground/webui/blueprints/activity_blueprint.py +7 -7
  16. geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +68 -22
  17. geo_activity_playground/webui/blueprints/summary_blueprint.py +8 -0
  18. geo_activity_playground/webui/templates/activity/show.html.j2 +3 -3
  19. geo_activity_playground/webui/templates/page.html.j2 +5 -1
  20. geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +63 -0
  21. geo_activity_playground/webui/templates/plot_builder/index.html.j2 +23 -35
  22. geo_activity_playground/webui/templates/summary/index.html.j2 +12 -0
  23. {geo_activity_playground-0.39.1.dist-info → geo_activity_playground-0.40.1.dist-info}/METADATA +1 -1
  24. {geo_activity_playground-0.39.1.dist-info → geo_activity_playground-0.40.1.dist-info}/RECORD +27 -18
  25. {geo_activity_playground-0.39.1.dist-info → geo_activity_playground-0.40.1.dist-info}/LICENSE +0 -0
  26. {geo_activity_playground-0.39.1.dist-info → geo_activity_playground-0.40.1.dist-info}/WHEEL +0 -0
  27. {geo_activity_playground-0.39.1.dist-info → geo_activity_playground-0.40.1.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
@@ -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:
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:
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
@@ -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
@@ -1,23 +1,16 @@
1
- import dataclasses
2
- from typing import Optional
3
-
4
1
  import altair as alt
5
2
  import pandas as pd
6
3
 
7
-
8
- @dataclasses.dataclass
9
- class ParametricPlotSpec:
10
- mark: str
11
- x: str
12
- y: str
13
- color: Optional[str]
14
- shape: Optional[str]
15
- size: Optional[str]
16
- row: Optional[str]
17
- column: Optional[str]
4
+ from .datamodel import PlotSpec
18
5
 
19
6
 
20
- MARKS = {"point": "Point", "circle": "Circle", "area": "Area", "bar": "Bar"}
7
+ MARKS = {
8
+ "point": "Point",
9
+ "circle": "Circle",
10
+ "area": "Area",
11
+ "bar": "Bar",
12
+ "rect": "Rectangle",
13
+ }
21
14
  CONTINUOUS_VARIABLES = {
22
15
  "distance_km": "Distance / km",
23
16
  "sum(distance_km)": "Total distance / km",
@@ -25,43 +18,44 @@ CONTINUOUS_VARIABLES = {
25
18
  "start": "Date",
26
19
  "hours": "Elapsed time / h",
27
20
  "hours_moving": "Moving time / h",
28
- "start_latitude": "Start latitude / °",
29
- "start_longitude": "Start longitude / °",
30
- "end_latitude": "End latitude / °",
31
- "end_longitude": "End longitude / °",
21
+ "calories": "Energy / kcal",
22
+ "steps": "Steps",
23
+ "elevation_gain": "Elevation gain / m",
32
24
  "start_elevation": "Start elevation / m",
33
25
  "end_elevation": "End elevation / m",
34
- "elevation_gain": "Elevation gain / m",
35
26
  "sum(elevation_gain)": "Total elevation gain / m",
36
27
  "mean(elevation_gain)": "Average elevation gain / m",
37
- "calories": "Energy / kcal",
38
- "steps": "Steps",
39
28
  "num_new_tiles_14": "New tiles 14",
40
29
  "num_new_tiles_14": "New tiles 17",
41
30
  "average_speed_moving_kmh": "Average moving speed / km/h",
42
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 / °",
43
36
  }
44
37
  DISCRETE_VARIABLES = {
45
- "": "",
46
- "year(start)": "Year",
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",
47
43
  "yearquarter(start)": "Year, Quarter",
48
44
  "yearquartermonth(start)": "Year, Quarter, Month",
49
45
  "yearmonth(start)": "Year, Month",
50
46
  "quarter(start)": "Quarter",
51
47
  "quartermonth(start)": "Quarter, Month",
52
48
  "month(start)": "Month",
49
+ "week:O": "ISO Week",
53
50
  "date(start)": "Day of month",
54
51
  "weekday(start)": "Day of week",
55
- "iso_year": "ISO Year",
56
- "week": "ISO Week",
57
- "equipment": "Equipment",
58
- "kind": "Activity kind",
59
- "consider_for_achievements": "Consider for achievements",
60
52
  }
61
- ALL_VARIABLES = {**DISCRETE_VARIABLES, **CONTINUOUS_VARIABLES}
53
+
54
+ VARIABLES_1 = {"": "", **DISCRETE_VARIABLES}
55
+ VARIABLES_2 = {"": "", **DISCRETE_VARIABLES, **CONTINUOUS_VARIABLES}
62
56
 
63
57
 
64
- def make_parametric_plot(df: pd.DataFrame, spec: ParametricPlotSpec) -> str:
58
+ def make_parametric_plot(df: pd.DataFrame, spec: PlotSpec) -> str:
65
59
  chart = alt.Chart(df)
66
60
 
67
61
  match spec.mark:
@@ -73,29 +67,42 @@ def make_parametric_plot(df: pd.DataFrame, spec: ParametricPlotSpec) -> str:
73
67
  chart = chart.mark_area()
74
68
  case "bar":
75
69
  chart = chart.mark_bar()
70
+ case "rect":
71
+ chart = chart.mark_rect()
72
+ case _:
73
+ raise ValueError()
76
74
 
77
75
  encodings = [
78
- alt.X(spec.x, title=ALL_VARIABLES[spec.x]),
79
- alt.Y(spec.y, title=ALL_VARIABLES[spec.y]),
76
+ alt.X(spec.x, title=VARIABLES_2[spec.x]),
77
+ alt.Y(spec.y, title=VARIABLES_2[spec.y]),
80
78
  ]
81
79
  tooltips = [
82
- alt.Tooltip(spec.x, title=ALL_VARIABLES[spec.x]),
83
- alt.Tooltip(spec.y, title=ALL_VARIABLES[spec.y]),
80
+ alt.Tooltip(spec.x, title=VARIABLES_2[spec.x]),
81
+ alt.Tooltip(spec.y, title=VARIABLES_2[spec.y]),
84
82
  ]
83
+
85
84
  if spec.color:
86
- encodings.append(alt.Color(spec.color, title=ALL_VARIABLES[spec.color]))
87
- tooltips.append(alt.Tooltip(spec.color, title=ALL_VARIABLES[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]))
88
87
  if spec.shape:
89
- encodings.append(alt.Shape(spec.shape, title=ALL_VARIABLES[spec.shape]))
90
- tooltips.append(alt.Tooltip(spec.shape, title=ALL_VARIABLES[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]))
91
90
  if spec.size:
92
- encodings.append(alt.Size(spec.size, title=ALL_VARIABLES[spec.size]))
93
- tooltips.append(alt.Tooltip(spec.size, title=ALL_VARIABLES[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]))
94
96
  if spec.row:
95
- encodings.append(alt.Row(spec.row, title=ALL_VARIABLES[spec.row]))
96
- tooltips.append(alt.Tooltip(spec.row, title=ALL_VARIABLES[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]))
97
99
  if spec.column:
98
- encodings.append(alt.Column(spec.column, title=ALL_VARIABLES[spec.column]))
99
- tooltips.append(alt.Tooltip(spec.column, title=ALL_VARIABLES[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]))
100
107
 
101
- return chart.encode(*encodings).interactive().to_json(format="vega")
108
+ return chart.encode(*encodings, tooltips).interactive().to_json(format="vega")
@@ -0,0 +1,20 @@
1
+ import datetime
2
+
3
+ from .datamodel import Activity
4
+
5
+
6
+ def test_no_duration() -> None:
7
+ activity = Activity(name="Test", distance_km=10.0)
8
+ assert activity.average_speed_elapsed_kmh is None
9
+ assert activity.average_speed_moving_kmh is None
10
+
11
+
12
+ def test_zero_duration() -> None:
13
+ activity = Activity(
14
+ name="Test",
15
+ distance_km=10.0,
16
+ elapsed_time=datetime.timedelta(seconds=0),
17
+ moving_time=datetime.timedelta(seconds=0),
18
+ )
19
+ assert activity.average_speed_elapsed_kmh is None
20
+ 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,15 +258,21 @@ 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):
264
271
  continue
265
272
 
266
273
  activity_date = dateutil.parser.parse(row["Activity Date"])
267
- activity_name = row["Activity Name"]
274
+ activity_name: str = row["Activity Name"]
268
275
  activity_kind = row["Activity Type"]
269
- is_commute = row["Commute"] == "true" or row["Commute"] == True
270
276
  equipment = (
271
277
  nan_as_none(row["Activity Gear"])
272
278
  or nan_as_none(row["Bike"])
@@ -278,8 +284,6 @@ def convert_strava_checkout(
278
284
  activity_target = playground_path / "Activities" / str(activity_kind)
279
285
  if equipment:
280
286
  activity_target /= str(equipment)
281
- if is_commute:
282
- activity_target /= "Commute"
283
287
 
284
288
  activity_target /= "".join(
285
289
  [
@@ -287,7 +291,7 @@ def convert_strava_checkout(
287
291
  "-",
288
292
  f"{activity_date.hour:02d}-{activity_date.minute:02d}-{activity_date.second:02d}",
289
293
  " ",
290
- activity_name,
294
+ activity_name.replace("/", "_"),
291
295
  ]
292
296
  + activity_file.suffixes
293
297
  )
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
@@ -44,6 +45,9 @@ from .flasher import FlaskFlasher
44
45
  from .search_util import SearchQueryHistory
45
46
 
46
47
 
48
+ logger = logging.getLogger(__name__)
49
+
50
+
47
51
  def get_secret_key():
48
52
  secret_file = pathlib.Path("Cache/flask-secret.json")
49
53
  if secret_file.exists():
@@ -66,9 +70,9 @@ def web_ui_main(
66
70
 
67
71
  app = Flask(__name__)
68
72
 
69
- app.config["SQLALCHEMY_DATABASE_URI"] = (
70
- f"sqlite:///{basedir.absolute()}/database.sqlite"
71
- )
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()}"
72
76
  app.config["ALEMBIC"] = {"script_location": "../alembic/versions"}
73
77
  DB.init_app(app)
74
78
 
@@ -138,7 +142,9 @@ def web_ui_main(
138
142
  "/heatmap": make_heatmap_blueprint(
139
143
  repository, tile_visit_accessor, config_accessor(), search_query_history
140
144
  ),
141
- "/plot-builder": make_plot_builder_blueprint(repository),
145
+ "/plot-builder": make_plot_builder_blueprint(
146
+ repository, flasher, authenticator
147
+ ),
142
148
  "/settings": make_settings_blueprint(config_accessor, authenticator, flasher),
143
149
  "/square-planner": make_square_planner_blueprint(tile_visit_accessor),
144
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
  )
@@ -1,43 +1,89 @@
1
+ import sqlalchemy
1
2
  from flask import Blueprint
3
+ from flask import redirect
2
4
  from flask import render_template
3
5
  from flask import request
4
6
  from flask import Response
7
+ from flask import url_for
5
8
 
6
9
  from ...core.activities import ActivityRepository
7
- from ...core.parametric_plot import ALL_VARIABLES
8
- from ...core.parametric_plot import CONTINUOUS_VARIABLES
9
- from ...core.parametric_plot import DISCRETE_VARIABLES
10
+ from ...core.datamodel import DB
10
11
  from ...core.parametric_plot import make_parametric_plot
11
12
  from ...core.parametric_plot import MARKS
12
- from ...core.parametric_plot import ParametricPlotSpec
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
13
20
 
14
21
 
15
- def make_plot_builder_blueprint(repository: ActivityRepository) -> Blueprint:
22
+ def make_plot_builder_blueprint(
23
+ repository: ActivityRepository, flasher: Flasher, authenticator: Authenticator
24
+ ) -> Blueprint:
16
25
  blueprint = Blueprint("plot_builder", __name__, template_folder="templates")
17
26
 
18
27
  @blueprint.route("/")
19
28
  def index() -> Response:
20
- context = {}
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)
21
52
  if request.args:
22
- spec = ParametricPlotSpec(
23
- mark=request.args["mark"],
24
- x=request.args["x"],
25
- y=request.args["y"],
26
- color=request.args.get("color", None),
27
- shape=request.args.get("shape", None),
28
- size=request.args.get("size", None),
29
- row=request.args.get("row", None),
30
- column=request.args.get("column", None),
31
- )
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:
32
66
  plot = make_parametric_plot(repository.meta, spec)
33
- context["plot"] = plot
67
+ DB.session.commit()
68
+ except ValueError as e:
69
+ plot = None
70
+ flasher.flash_message(str(e), FlashTypes.WARNING)
34
71
  return render_template(
35
- "plot_builder/index.html.j2",
72
+ "plot_builder/edit.html.j2",
36
73
  marks=MARKS,
37
- continuous=ALL_VARIABLES,
38
- discrete=DISCRETE_VARIABLES,
39
- **context,
40
- **request.args
74
+ discrete=VARIABLES_1,
75
+ continuous=VARIABLES_2,
76
+ plot=plot,
77
+ spec=spec,
41
78
  )
42
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
+
43
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 %}
@@ -1,44 +1,32 @@
1
1
  {% extends "page.html.j2" %}
2
- {% from "search_form.html.j2" import search_form %}
3
2
 
4
3
  {% block container %}
5
4
 
6
5
  <h1 class="row mb-3">Plot Builder</h1>
7
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 %}
8
29
 
9
- <div class="row">
10
- <div class="col-md-4">
11
- {% macro select_field(label, name, choices, active) %}
12
- <div class="mb-3">
13
- <label for="{{ name }}">{{ label }}</label>
14
- <select name="{{ name }}" class="form-select">
15
- {% for choice_value, choice_label in choices.items() %}
16
- <option id="{{ name }}" value="{{ choice_value }}" {% if active==choice_value %} selected {% endif %}>
17
- {{ choice_label }}
18
- </option>
19
- {% endfor %}
20
- </select>
21
- </div>
22
- {% endmacro %}
30
+ <a class="btn btn-primary" href="{{ url_for('.new') }}">New</a>
23
31
 
24
- <form>
25
- {{ select_field("Mark", "mark", marks, mark)}}
26
-
27
- {{ select_field("X", "x", continuous, x)}}
28
- {{ select_field("Y", "y", continuous, y)}}
29
- {{ select_field("Color", "color", continuous, color)}}
30
- {{ select_field("Size", "size", continuous, size)}}
31
- {{ select_field("Shape", "shape", discrete, shape)}}
32
- {{ select_field("Row", "row", discrete, row)}}
33
- {{ select_field("Column", "column", discrete, column)}}
34
-
35
- <button type="submit" class="btn btn-primary">Submit</button>
36
- </form>
37
- </div>
38
- <div class="col-md-8">
39
- {% if plot %}
40
- {{ vega_direct("plot", plot) }}
41
- {% endif %}
42
- </div>
43
- </div>
44
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.1
3
+ Version: 0.40.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -1,48 +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
13
+ geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
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=EeMcwZ2xbqQRwo6R7lOJwbc9iIXnpgg2E4eBdsAbk24,8685
15
- geo_activity_playground/core/enrichment.py,sha256=LWAKF6mAKtiZcIF8ptAgpt8fYTYZAnSuXGHuX5TTO6o,7220
17
+ geo_activity_playground/core/datamodel.py,sha256=EHfZEYL4Dc881R5fL7eiJ24867NYgAbdXtRYI_I-sPI,10279
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
18
- geo_activity_playground/core/parametric_plot.py,sha256=JdG2HvQHn5mMY1pfcebdgajzpp0CD7KvaHyeaJodDsE,3538
21
+ geo_activity_playground/core/parametric_plot.py,sha256=IefPc6lwthxowvjUDA5wu23oBSw9jq399l04gSaNrOQ,3880
19
22
  geo_activity_playground/core/paths.py,sha256=aUXGuNn9hBvGPQWPoUJeImHN0PB0fS1tja1tm2eq8mA,2595
20
23
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
21
24
  geo_activity_playground/core/raster_map.py,sha256=Cq8dNLdxVQg3Agzn2bmXVu0-8kZf56QrSe-LKNn3jaU,7994
22
25
  geo_activity_playground/core/similarity.py,sha256=L2de3DPRdDeDY5AxZwLDcH7FjHWRWklr41VNU06q9kQ,3117
23
26
  geo_activity_playground/core/summary_stats.py,sha256=v5FtWnE1imDF5axI6asVN55wCrlD73oZ6lvqzxsTN2c,1006
24
27
  geo_activity_playground/core/tasks.py,sha256=-_9cxekoHSWzCW4XblNeqrwi2tTqr5AE7_-p8fdqhwc,2886
28
+ geo_activity_playground/core/test_datamodel.py,sha256=-VrGHgx5Z3MSQPqHGmmm7atRJYbg5y_ukvRHKxk22PI,569
25
29
  geo_activity_playground/core/test_meta_search.py,sha256=zhuD343Xce-4Fkznw81DHQ7pK5eyX5UbcyCHuYRKsr8,3091
26
30
  geo_activity_playground/core/test_summary_stats.py,sha256=qH_45mPRFD2H-Rr0Ku-RYc67vhC7qKxbPr7J2F36uV8,3081
27
31
  geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
28
32
  geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
29
33
  geo_activity_playground/core/tiles.py,sha256=lV6X1Uc9XQecu2LALIvxpnMcLsVtWx7JczJ5a_S1eZE,2139
30
34
  geo_activity_playground/core/time_conversion.py,sha256=x5mXG6Y4GtdX7CBmwucGNSWBp9JQJDbZ7u0JkdUY1Vs,379
35
+ geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
36
  geo_activity_playground/explorer/grid_file.py,sha256=YNL_c4O1-kxaajATJwj4ZLywCL5Hpj9qy2h-F7rk8Yg,3260
32
37
  geo_activity_playground/explorer/tile_visits.py,sha256=C8IpAGmrjMGYhyTVK-tl2ptM9-CXF2mwibhJYn7gLf8,13905
33
38
  geo_activity_playground/explorer/video.py,sha256=7j6Qv3HG6On7Tn7xh7Olwrx_fbQnfzS7CeRg3TEApHg,4397
34
39
  geo_activity_playground/heatmap_video.py,sha256=I8i1uVvbbPUXVtvLAROaLy58nQoUPnuMCZkERWNkQjg,3318
35
- 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
36
42
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
37
43
  geo_activity_playground/importers/directory.py,sha256=4Q7UAFa7ztkgqf4FvPbH2LlrO-7a8Fu7tkYPHOpHm1g,5210
38
44
  geo_activity_playground/importers/strava_api.py,sha256=J0-VXNrLq22fhTcWkQPE5AVrzy5aegC7SBi-UXFtAy4,7576
39
- geo_activity_playground/importers/strava_checkout.py,sha256=EixNFXXnxkopqUs0qe6iShYTbGu_o_g2_1lxuUIsz4E,9679
45
+ geo_activity_playground/importers/strava_checkout.py,sha256=3AEFlNr811MdVz_Ww1zz_alptr_3mDvGr188bM3qU7U,9977
40
46
  geo_activity_playground/importers/test_csv_parser.py,sha256=nOTVTdlzIY0TDcbWp7xNyNaIO6Mkeu55hVziVl22QE4,1092
41
47
  geo_activity_playground/importers/test_directory.py,sha256=_fn_-y98ZyElbG0BRxAmGFdtGobUShPU86SdEOpuv-A,691
42
48
  geo_activity_playground/importers/test_strava_api.py,sha256=7b8bl5Rh2BctCmvTPEhCadxtUOq3mfzuadD6F5XxRio,398
43
- geo_activity_playground/webui/app.py,sha256=h1AULky6kMxxmi54h3aTaE8rtJjOeRoCXIYI4gFNicU,7013
49
+ geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ geo_activity_playground/webui/app.py,sha256=xoRnF2ISq74v48ZBpyrCxEFPCwtS4fw3WZvWbvExSmE,7208
44
51
  geo_activity_playground/webui/authenticator.py,sha256=jtQqvpVHa_eLTAulmvvJgDRoCWOEege49G9zn3MfYk8,1394
45
- 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
46
54
  geo_activity_playground/webui/blueprints/auth_blueprint.py,sha256=_VZeP3VN626BoOOZUkNVnuw9v-cEOrkHz5lhFPmxqMY,784
47
55
  geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py,sha256=tiqDf-DZLdjm8faatZ5fqbh7gkII2KmBPRtkcKqoLIA,2194
48
56
  geo_activity_playground/webui/blueprints/calendar_blueprint.py,sha256=L6R8xieYYXeEMDzJs-MjWax1JAHhppWy3r3U8MyCOAk,2585
@@ -51,11 +59,11 @@ geo_activity_playground/webui/blueprints/entry_views.py,sha256=-CRrE5b9QYyrmQhLY
51
59
  geo_activity_playground/webui/blueprints/equipment_blueprint.py,sha256=juQ5L2BlrECb00LBbiY2yc0b8W_B9Y3fPwtbiaRfgpo,5634
52
60
  geo_activity_playground/webui/blueprints/explorer_blueprint.py,sha256=ZNDRwtmYvWGyqDe3fszGp0DR5qzRRxRss32ZGPRuXsY,12469
53
61
  geo_activity_playground/webui/blueprints/heatmap_blueprint.py,sha256=iHI5YJYhX7ZOlzTgzl2efIRDzt3UMYCx7X4-LVd0MWk,8702
54
- geo_activity_playground/webui/blueprints/plot_builder_blueprint.py,sha256=EWLsNNIAl7eK73WVIi5Tneqjg0QVUZXoM1lScldr4ck,1544
62
+ geo_activity_playground/webui/blueprints/plot_builder_blueprint.py,sha256=7HrjpBM-608HSOh0i31Lmt7yDNMfWlEn6G7DlYqlV9w,3031
55
63
  geo_activity_playground/webui/blueprints/search_blueprint.py,sha256=Sv_KL1Cdai26y51qVfI-5jZLhtElREsEar1dbR_VAC4,2275
56
64
  geo_activity_playground/webui/blueprints/settings_blueprint.py,sha256=UUv63BDQFnBPq8fLDdlWHd5mxL5qIgcGUuqQRFemyEA,16108
57
65
  geo_activity_playground/webui/blueprints/square_planner_blueprint.py,sha256=xVaxJxmt8Dysl3UL9f2y__LVLtTH2Np1Ust4OSXKRAk,4746
58
- 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
59
67
  geo_activity_playground/webui/blueprints/tile_blueprint.py,sha256=YzZf9OrNdjhc1_j4MtO1DMcw1uCv29ueNsYd-mWqgbg,837
60
68
  geo_activity_playground/webui/blueprints/upload_blueprint.py,sha256=_VeGu08vlRZlRn5J4t7VdBk2TTW5GXB4JUcge9mbX9Y,4111
61
69
  geo_activity_playground/webui/flasher.py,sha256=Covc1D9cO_jjokRWnvyiXCc2tfp3aZ8XkNqFdA1AXtk,500
@@ -99,7 +107,7 @@ geo_activity_playground/webui/templates/activity/day.html.j2,sha256=CHEvxlZralCm
99
107
  geo_activity_playground/webui/templates/activity/edit.html.j2,sha256=9HDFjYfUQBB6HAgeIZppFPlpiJ1vDZWcGyP7uYG_Hnw,1369
100
108
  geo_activity_playground/webui/templates/activity/lines.html.j2,sha256=_ZDg1ruW-9UMJfOudy1-uY_-IcSSaagq7tPCih5Bb8g,1079
101
109
  geo_activity_playground/webui/templates/activity/name.html.j2,sha256=7Wbh3IrVL5lMRve467H0P10Shn5FzGpaXLhV0H-X4Hk,2725
102
- 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
103
111
  geo_activity_playground/webui/templates/activity/trim.html.j2,sha256=3oAXQab6QqWjGBC9KCvWNOVn8uRmxoDLj3hx_O63TXc,1836
104
112
  geo_activity_playground/webui/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
105
113
  geo_activity_playground/webui/templates/bubble_chart/index.html.j2,sha256=pqyafhIgL97FuD4_-lMb8lRWC3rejwrjawbmfp17XFY,1143
@@ -110,8 +118,9 @@ geo_activity_playground/webui/templates/equipment/index.html.j2,sha256=wwrGmfCCB
110
118
  geo_activity_playground/webui/templates/explorer/index.html.j2,sha256=3t9ikAF6oMvEaVlS3Kb1tj9ngomIQlatzqPnqVsEDKA,6908
111
119
  geo_activity_playground/webui/templates/heatmap/index.html.j2,sha256=kTVvEt-GmSNebDlVMa6zwyIuP0mJcZQFuqj-IY8JV5U,1359
112
120
  geo_activity_playground/webui/templates/home.html.j2,sha256=vQp9uMn7BLY7pexWJVpQVWN8ZbbtWZvkW_hYSkYQeZs,2212
113
- geo_activity_playground/webui/templates/page.html.j2,sha256=TSNEZNLFzJ76G_22dDIXoTozfdWjmKk03qZl6exsdCQ,11043
114
- geo_activity_playground/webui/templates/plot_builder/index.html.j2,sha256=JQvXHVnPqA6Vlp2HfOt8_LhzvJ_eH5xTxe8-51sMkQk,1493
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
115
124
  geo_activity_playground/webui/templates/search/index.html.j2,sha256=_kxTgsdbT8o-4ryW0pvyWE7a-rOs7xzGUpdSPp8Q1is,1320
116
125
  geo_activity_playground/webui/templates/search_form.html.j2,sha256=TG9xIql0HnhsXtbHZxl3GLBt6cGYjA8jPeBq11syQ3A,6512
117
126
  geo_activity_playground/webui/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
@@ -126,11 +135,11 @@ geo_activity_playground/webui/templates/settings/segmentation.html.j2,sha256=QV7
126
135
  geo_activity_playground/webui/templates/settings/sharepic.html.j2,sha256=qZkfEpd4CtKKMaSSVadqvNEgMRYLV-0X-pw5-nJvukk,678
127
136
  geo_activity_playground/webui/templates/settings/strava.html.j2,sha256=GCE5gskQ6xJ8AM1qGrrUVLDOiuqg510mWzzsZjia0gk,2211
128
137
  geo_activity_playground/webui/templates/square_planner/index.html.j2,sha256=-OnY2nQCgZCslOzf28ogZwFykwF8tZm7PgFwOE3eBDk,8176
129
- 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
130
139
  geo_activity_playground/webui/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
131
140
  geo_activity_playground/webui/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
132
- geo_activity_playground-0.39.1.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
133
- geo_activity_playground-0.39.1.dist-info/METADATA,sha256=ez-3_BF1sUiuV-TSZ7pO0yIjHw6u0BBxJtVQ8aITGn4,1758
134
- geo_activity_playground-0.39.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
135
- geo_activity_playground-0.39.1.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
136
- geo_activity_playground-0.39.1.dist-info/RECORD,,
141
+ geo_activity_playground-0.40.1.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
142
+ geo_activity_playground-0.40.1.dist-info/METADATA,sha256=A-lrCQSSUmsBauIvH6dFq3Gbn7nlKQmstaPaN78TInw,1758
143
+ geo_activity_playground-0.40.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
144
+ geo_activity_playground-0.40.1.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
145
+ geo_activity_playground-0.40.1.dist-info/RECORD,,