geo-activity-playground 0.40.0__py3-none-any.whl → 0.41.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 (30) hide show
  1. geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +70 -0
  2. geo_activity_playground/alembic/versions/script.py.mako +0 -6
  3. geo_activity_playground/core/activities.py +17 -30
  4. geo_activity_playground/core/datamodel.py +83 -2
  5. geo_activity_playground/core/test_datamodel.py +14 -1
  6. geo_activity_playground/importers/strava_checkout.py +2 -5
  7. geo_activity_playground/webui/app.py +6 -2
  8. geo_activity_playground/webui/blueprints/activity_blueprint.py +20 -3
  9. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +50 -25
  10. geo_activity_playground/webui/blueprints/calendar_blueprint.py +12 -4
  11. geo_activity_playground/webui/blueprints/eddington_blueprints.py +253 -0
  12. geo_activity_playground/webui/blueprints/entry_views.py +30 -15
  13. geo_activity_playground/webui/blueprints/explorer_blueprint.py +83 -9
  14. geo_activity_playground/webui/blueprints/summary_blueprint.py +102 -42
  15. geo_activity_playground/webui/columns.py +37 -0
  16. geo_activity_playground/webui/templates/activity/show.html.j2 +15 -4
  17. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +24 -8
  18. geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +150 -0
  19. geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +150 -0
  20. geo_activity_playground/webui/templates/explorer/server-side.html.j2 +72 -0
  21. geo_activity_playground/webui/templates/home.html.j2 +14 -5
  22. geo_activity_playground/webui/templates/page.html.j2 +10 -1
  23. geo_activity_playground/webui/templates/summary/index.html.j2 +91 -2
  24. {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/METADATA +1 -1
  25. {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/RECORD +29 -24
  26. geo_activity_playground/webui/blueprints/eddington_blueprint.py +0 -194
  27. /geo_activity_playground/webui/templates/eddington/{index.html.j2 → distance.html.j2} +0 -0
  28. {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/LICENSE +0 -0
  29. {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/WHEEL +0 -0
  30. {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,70 @@
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 = "38882503dc7c"
10
+ down_revision: Union[str, None] = "93cc82ad1b60"
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
+ "tags",
19
+ sa.Column("id", sa.Integer(), nullable=False),
20
+ sa.Column("tag", sa.String(), nullable=False),
21
+ sa.PrimaryKeyConstraint("id"),
22
+ sa.UniqueConstraint("tag"),
23
+ sa.UniqueConstraint("tag", name="tags_tag"),
24
+ )
25
+ op.create_table(
26
+ "activity_tag_association_table",
27
+ sa.Column("left_id", sa.Integer(), nullable=False),
28
+ sa.Column("right_id", sa.Integer(), nullable=False),
29
+ sa.ForeignKeyConstraint(
30
+ ["left_id"],
31
+ ["activities.id"],
32
+ ),
33
+ sa.ForeignKeyConstraint(
34
+ ["right_id"],
35
+ ["tags.id"],
36
+ ),
37
+ sa.PrimaryKeyConstraint("left_id", "right_id"),
38
+ )
39
+ with op.batch_alter_table("plot_specs", schema=None) as batch_op:
40
+ batch_op.alter_column("mark", existing_type=sa.VARCHAR(), nullable=False)
41
+ batch_op.alter_column("x", existing_type=sa.VARCHAR(), nullable=False)
42
+ batch_op.alter_column("y", existing_type=sa.VARCHAR(), nullable=False)
43
+ batch_op.alter_column("color", existing_type=sa.VARCHAR(), nullable=False)
44
+ batch_op.alter_column("shape", existing_type=sa.VARCHAR(), nullable=False)
45
+ batch_op.alter_column("size", existing_type=sa.VARCHAR(), nullable=False)
46
+ batch_op.alter_column("row", existing_type=sa.VARCHAR(), nullable=False)
47
+ batch_op.alter_column("opacity", existing_type=sa.VARCHAR(), nullable=False)
48
+ batch_op.alter_column("column", existing_type=sa.VARCHAR(), nullable=False)
49
+ batch_op.alter_column("facet", existing_type=sa.VARCHAR(), nullable=False)
50
+
51
+ # ### end Alembic commands ###
52
+
53
+
54
+ def downgrade() -> None:
55
+ # ### commands auto generated by Alembic - please adjust! ###
56
+ with op.batch_alter_table("plot_specs", schema=None) as batch_op:
57
+ batch_op.alter_column("facet", existing_type=sa.VARCHAR(), nullable=True)
58
+ batch_op.alter_column("column", existing_type=sa.VARCHAR(), nullable=True)
59
+ batch_op.alter_column("opacity", existing_type=sa.VARCHAR(), nullable=True)
60
+ batch_op.alter_column("row", existing_type=sa.VARCHAR(), nullable=True)
61
+ batch_op.alter_column("size", existing_type=sa.VARCHAR(), nullable=True)
62
+ batch_op.alter_column("shape", existing_type=sa.VARCHAR(), nullable=True)
63
+ batch_op.alter_column("color", existing_type=sa.VARCHAR(), nullable=True)
64
+ batch_op.alter_column("y", existing_type=sa.VARCHAR(), nullable=True)
65
+ batch_op.alter_column("x", existing_type=sa.VARCHAR(), nullable=True)
66
+ batch_op.alter_column("mark", existing_type=sa.VARCHAR(), nullable=True)
67
+
68
+ op.drop_table("activity_tag_association_table")
69
+ op.drop_table("tags")
70
+ # ### end Alembic commands ###
@@ -1,10 +1,4 @@
1
- """${message}
2
1
 
3
- Revision ID: ${up_revision}
4
- Revises: ${down_revision | comma,n}
5
- Create Date: ${create_date}
6
-
7
- """
8
2
  from typing import Sequence, Union
9
3
 
10
4
  from alembic import op
@@ -16,6 +16,7 @@ from geo_activity_playground.core.datamodel import Activity
16
16
  from geo_activity_playground.core.datamodel import ActivityMeta
17
17
  from geo_activity_playground.core.datamodel import DB
18
18
  from geo_activity_playground.core.datamodel import Kind
19
+ from geo_activity_playground.core.datamodel import query_activity_meta
19
20
 
20
21
  logger = logging.getLogger(__name__)
21
22
 
@@ -79,22 +80,8 @@ class ActivityRepository:
79
80
 
80
81
  @property
81
82
  def meta(self) -> pd.DataFrame:
82
- activities = self.iter_activities(new_to_old=False, drop_na=True)
83
- df = pd.DataFrame([activity.to_dict() for activity in activities])
84
- df["date"] = df["start"].dt.date
85
- df["year"] = [start.year for start in df["start"]]
86
- df["month"] = [start.month for start in df["start"]]
87
- df["day"] = [start.day for start in df["start"]]
88
- df["week"] = [start.isocalendar().week for start in df["start"]]
89
- df["day_of_week"] = df["start"].dt.day_of_week
90
- df["iso_year"] = [start.isocalendar().year for start in df["start"]]
91
- df["hours"] = [
92
- elapsed_time.total_seconds() / 3600 for elapsed_time in df["elapsed_time"]
93
- ]
94
- df["hours_moving"] = [
95
- moving_time.total_seconds() / 3600 for moving_time in df["moving_time"]
96
- ]
97
- df.index = df["id"]
83
+ df = query_activity_meta()
84
+
98
85
  return df
99
86
 
100
87
 
@@ -114,8 +101,8 @@ def inter_quartile_range(values):
114
101
  return np.quantile(values, 0.75) - np.quantile(values, 0.25)
115
102
 
116
103
 
117
- def make_geojson_color_line(time_series: pd.DataFrame) -> str:
118
- low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
104
+ def make_geojson_color_line(time_series: pd.DataFrame, column: str) -> str:
105
+ low, high, clamp_value = _make_value_clamp(time_series[column])
119
106
  cmap = matplotlib.colormaps["viridis"]
120
107
  features = [
121
108
  geojson.Feature(
@@ -126,8 +113,8 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
126
113
  ]
127
114
  ),
128
115
  properties={
129
- "speed": next_row["speed"] if np.isfinite(next_row["speed"]) else 0.0,
130
- "color": matplotlib.colors.to_hex(cmap(clamp_speed(next_row["speed"]))),
116
+ column: (next_row[column] if np.isfinite(next_row[column]) else 0.0),
117
+ "color": matplotlib.colors.to_hex(cmap(clamp_value(next_row[column]))),
131
118
  },
132
119
  )
133
120
  for _, group in time_series.groupby("segment_id")
@@ -137,21 +124,21 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
137
124
  return geojson.dumps(feature_collection)
138
125
 
139
126
 
140
- def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
141
- low, high, clamp_speed = _make_speed_clamp(time_series["speed"])
127
+ def make_color_bar(time_series: pd.Series, format: str) -> dict[str, Any]:
128
+ low, high, clamp_value = _make_value_clamp(time_series)
142
129
  cmap = matplotlib.colormaps["viridis"]
143
130
  colors = [
144
- (f"{speed:.1f}", matplotlib.colors.to_hex(cmap(clamp_speed(speed))))
145
- for speed in np.linspace(low, high, 10)
131
+ (f"{value:{format}}", matplotlib.colors.to_hex(cmap(clamp_value(value))))
132
+ for value in np.linspace(low, high, 10)
146
133
  ]
147
134
  return {"low": low, "high": high, "colors": colors}
148
135
 
149
136
 
150
- def _make_speed_clamp(speeds: pd.Series) -> tuple[float, float, Callable]:
151
- speed_without_na = speeds.dropna()
152
- low = min(speed_without_na)
137
+ def _make_value_clamp(values: pd.Series) -> tuple[float, float, Callable]:
138
+ values_without_na = values.dropna()
139
+ low = min(values_without_na)
153
140
  high = min(
154
- max(speed_without_na),
155
- np.median(speed_without_na) + 1.5 * inter_quartile_range(speed_without_na),
141
+ max(values_without_na),
142
+ np.median(values_without_na) + 1.5 * inter_quartile_range(values_without_na),
156
143
  )
157
- return low, high, lambda speed: min(max((speed - low) / (high - low), 0.0), 1.0)
144
+ return low, high, lambda value: min(max((value - low) / (high - low), 0.0), 1.0)
@@ -10,8 +10,10 @@ import pandas as pd
10
10
  import sqlalchemy
11
11
  import sqlalchemy as sa
12
12
  from flask_sqlalchemy import SQLAlchemy
13
+ from sqlalchemy import Column
13
14
  from sqlalchemy import ForeignKey
14
15
  from sqlalchemy import String
16
+ from sqlalchemy import Table
15
17
  from sqlalchemy.orm import DeclarativeBase
16
18
  from sqlalchemy.orm import Mapped
17
19
  from sqlalchemy.orm import mapped_column
@@ -53,6 +55,13 @@ class Base(DeclarativeBase):
53
55
 
54
56
  DB = SQLAlchemy(model_class=Base)
55
57
 
58
+ activity_tag_association_table = Table(
59
+ "activity_tag_association_table",
60
+ Base.metadata,
61
+ Column("left_id", ForeignKey("activities.id"), primary_key=True),
62
+ Column("right_id", ForeignKey("tags.id"), primary_key=True),
63
+ )
64
+
56
65
 
57
66
  class Activity(DB.Model):
58
67
  __tablename__ = "activities"
@@ -104,6 +113,10 @@ class Activity(DB.Model):
104
113
  )
105
114
  kind: Mapped["Kind"] = relationship(back_populates="activities")
106
115
 
116
+ tags: Mapped[list["Tag"]] = relationship(
117
+ secondary=activity_tag_association_table, back_populates="activities"
118
+ )
119
+
107
120
  def __getitem__(self, item) -> Any:
108
121
  return self.to_dict()[item]
109
122
 
@@ -112,12 +125,12 @@ class Activity(DB.Model):
112
125
 
113
126
  @property
114
127
  def average_speed_moving_kmh(self) -> Optional[float]:
115
- if self.moving_time is not None:
128
+ if self.moving_time:
116
129
  return self.distance_km / (self.moving_time.total_seconds() / 3_600)
117
130
 
118
131
  @property
119
132
  def average_speed_elapsed_kmh(self) -> Optional[float]:
120
- if self.elapsed_time is not None:
133
+ if self.elapsed_time:
121
134
  return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
122
135
 
123
136
  @property
@@ -174,6 +187,74 @@ class Activity(DB.Model):
174
187
  )
175
188
 
176
189
 
190
+ class Tag(DB.Model):
191
+ __tablename__ = "tags"
192
+ __table_args__ = (sa.UniqueConstraint("tag", name="tags_tag"),)
193
+
194
+ id: Mapped[int] = mapped_column(primary_key=True)
195
+ tag: Mapped[str] = mapped_column(String, unique=True)
196
+
197
+ activities: Mapped[list[Activity]] = relationship(
198
+ secondary=activity_tag_association_table, back_populates="tags"
199
+ )
200
+
201
+
202
+ def query_activity_meta() -> pd.DataFrame:
203
+ rows = DB.session.execute(
204
+ sqlalchemy.select(
205
+ Activity.id,
206
+ Activity.name,
207
+ Activity.path,
208
+ Activity.distance_km,
209
+ Activity.start,
210
+ Activity.elapsed_time,
211
+ Activity.moving_time,
212
+ Activity.start_latitude,
213
+ Activity.start_longitude,
214
+ Activity.end_latitude,
215
+ Activity.end_longitude,
216
+ Activity.elevation_gain,
217
+ Activity.start_elevation,
218
+ Activity.end_elevation,
219
+ Activity.calories,
220
+ Activity.steps,
221
+ Activity.num_new_tiles_14,
222
+ Activity.num_new_tiles_17,
223
+ Kind.consider_for_achievements,
224
+ Equipment.name.label("equipment"),
225
+ Kind.name.label("kind"),
226
+ )
227
+ .join(Activity.equipment)
228
+ .join(Activity.kind)
229
+ .order_by(Activity.start)
230
+ ).all()
231
+ df = pd.DataFrame(rows)
232
+
233
+ for old, new in [
234
+ ("elapsed_time", "average_speed_elapsed_kmh"),
235
+ ("moving_time", "average_speed_moving_kmh"),
236
+ ]:
237
+ df[new] = pd.NA
238
+ mask = df[old].dt.total_seconds() > 0
239
+ df.loc[mask, new] = df.loc[mask, "distance_km"] / (
240
+ df.loc[mask, old].dt.total_seconds() / 3_600
241
+ )
242
+
243
+ df["date"] = df["start"].dt.date
244
+ df["year"] = df["start"].dt.year
245
+ df["month"] = df["start"].dt.month
246
+ df["day"] = df["start"].dt.day
247
+ df["week"] = df["start"].dt.isocalendar().week
248
+ df["day_of_week"] = df["start"].dt.day_of_week
249
+ df["iso_year"] = df["start"].dt.isocalendar().year
250
+ df["hours"] = df["elapsed_time"].dt.total_seconds() / 3_600
251
+ df["hours_moving"] = df["moving_time"].dt.total_seconds() / 3_600
252
+
253
+ df.index = df["id"]
254
+
255
+ return df
256
+
257
+
177
258
  class Equipment(DB.Model):
178
259
  __tablename__ = "equipments"
179
260
 
@@ -1,7 +1,20 @@
1
+ import datetime
2
+
1
3
  from .datamodel import Activity
2
4
 
3
5
 
4
- def test_zero_duration() -> None:
6
+ def test_no_duration() -> None:
5
7
  activity = Activity(name="Test", distance_km=10.0)
6
8
  assert activity.average_speed_elapsed_kmh is None
7
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
@@ -271,9 +271,8 @@ def convert_strava_checkout(
271
271
  continue
272
272
 
273
273
  activity_date = dateutil.parser.parse(row["Activity Date"])
274
- activity_name = row["Activity Name"]
274
+ activity_name: str = row["Activity Name"]
275
275
  activity_kind = row["Activity Type"]
276
- is_commute = row["Commute"] == "true" or row["Commute"] == True
277
276
  equipment = (
278
277
  nan_as_none(row["Activity Gear"])
279
278
  or nan_as_none(row["Bike"])
@@ -285,8 +284,6 @@ def convert_strava_checkout(
285
284
  activity_target = playground_path / "Activities" / str(activity_kind)
286
285
  if equipment:
287
286
  activity_target /= str(equipment)
288
- if is_commute:
289
- activity_target /= "Commute"
290
287
 
291
288
  activity_target /= "".join(
292
289
  [
@@ -294,7 +291,7 @@ def convert_strava_checkout(
294
291
  "-",
295
292
  f"{activity_date.hour:02d}-{activity_date.minute:02d}-{activity_date.second:02d}",
296
293
  " ",
297
- activity_name,
294
+ activity_name.replace("/", "_"),
298
295
  ]
299
296
  + activity_file.suffixes
300
297
  )
@@ -28,7 +28,7 @@ from .blueprints.activity_blueprint import make_activity_blueprint
28
28
  from .blueprints.auth_blueprint import make_auth_blueprint
29
29
  from .blueprints.bubble_chart_blueprint import make_bubble_chart_blueprint
30
30
  from .blueprints.calendar_blueprint import make_calendar_blueprint
31
- from .blueprints.eddington_blueprint import register_eddington_blueprint
31
+ from .blueprints.eddington_blueprints import register_eddington_blueprint
32
32
  from .blueprints.entry_views import register_entry_views
33
33
  from .blueprints.equipment_blueprint import make_equipment_blueprint
34
34
  from .blueprints.explorer_blueprint import make_explorer_blueprint
@@ -137,7 +137,11 @@ def web_ui_main(
137
137
  "/eddington": register_eddington_blueprint(repository, search_query_history),
138
138
  "/equipment": make_equipment_blueprint(repository, config),
139
139
  "/explorer": make_explorer_blueprint(
140
- authenticator, repository, tile_visit_accessor, config_accessor
140
+ authenticator,
141
+ tile_visit_accessor,
142
+ config_accessor,
143
+ tile_getter,
144
+ image_transforms,
141
145
  ),
142
146
  "/heatmap": make_heatmap_blueprint(
143
147
  repository, tile_visit_accessor, config_accessor(), search_query_history
@@ -21,9 +21,9 @@ from PIL import Image
21
21
  from PIL import ImageDraw
22
22
 
23
23
  from ...core.activities import ActivityRepository
24
+ from ...core.activities import make_color_bar
24
25
  from ...core.activities import make_geojson_color_line
25
26
  from ...core.activities import make_geojson_from_time_series
26
- from ...core.activities import make_speed_color_bar
27
27
  from ...core.config import Config
28
28
  from ...core.datamodel import Activity
29
29
  from ...core.datamodel import DB
@@ -41,6 +41,8 @@ from ...explorer.grid_file import make_grid_points
41
41
  from ...explorer.tile_visits import TileVisitAccessor
42
42
  from ..authenticator import Authenticator
43
43
  from ..authenticator import needs_authentication
44
+ from ..columns import column_elevation
45
+ from ..columns import column_speed
44
46
 
45
47
  logger = logging.getLogger(__name__)
46
48
 
@@ -128,19 +130,34 @@ def make_activity_blueprint(
128
130
  new_tiles_geojson[zoom] = make_grid_file_geojson(points)
129
131
  new_tiles_per_zoom[zoom] = len(new_tiles)
130
132
 
133
+ line_color_columns_avail = dict(
134
+ [(column.name, column) for column in [column_speed, column_elevation]]
135
+ )
136
+ line_color_column = (
137
+ request.args.get("line_color_column")
138
+ or next(iter(line_color_columns_avail.values())).name
139
+ )
140
+
131
141
  context = {
132
142
  "activity": activity,
133
143
  "line_json": line_json,
134
144
  "distance_time_plot": distance_time_plot(time_series),
135
- "color_line_geojson": make_geojson_color_line(time_series),
145
+ "color_line_geojson": make_geojson_color_line(
146
+ time_series, line_color_column
147
+ ),
136
148
  "speed_time_plot": speed_time_plot(time_series),
137
149
  "speed_distribution_plot": speed_distribution_plot(time_series),
138
150
  "similar_activites": similar_activities,
139
- "speed_color_bar": make_speed_color_bar(time_series),
151
+ "line_color_bar": make_color_bar(
152
+ time_series[line_color_column],
153
+ line_color_columns_avail[line_color_column].format,
154
+ ),
140
155
  "date": activity["start"].date(),
141
156
  "time": activity["start"].time(),
142
157
  "new_tiles": new_tiles_per_zoom,
143
158
  "new_tiles_geojson": new_tiles_geojson,
159
+ "line_color_column": line_color_column,
160
+ "line_color_columns_avail": line_color_columns_avail,
144
161
  }
145
162
  if (
146
163
  heart_zones := _extract_heart_rate_zones(
@@ -3,6 +3,10 @@ import pandas as pd
3
3
  from flask import Blueprint
4
4
  from flask import render_template
5
5
 
6
+ from ..columns import column_distance
7
+ from ..columns import column_elevation_gain
8
+ from ..columns import ColumnDescription
9
+
6
10
 
7
11
  def make_bubble_chart_blueprint(repository) -> Blueprint:
8
12
  blueprint = Blueprint("bubble_chart", __name__, template_folder="templates")
@@ -19,11 +23,16 @@ def make_bubble_chart_blueprint(repository) -> Blueprint:
19
23
 
20
24
  # Prepare the bubble chart data
21
25
  bubble_data = activities[
22
- ["start", "distance_km", "kind", "activity_id"]
26
+ [
27
+ "start",
28
+ "kind",
29
+ "activity_id",
30
+ column_distance.name,
31
+ column_elevation_gain.name,
32
+ ]
23
33
  ].rename(
24
34
  columns={
25
35
  "start": "date",
26
- "distance_km": "distance",
27
36
  "kind": "activity",
28
37
  "activity_id": "id",
29
38
  }
@@ -33,29 +42,45 @@ def make_bubble_chart_blueprint(repository) -> Blueprint:
33
42
  lambda x: f"/activity/{x}"
34
43
  )
35
44
 
36
- # Create the bubble chart
37
- bubble_chart = (
38
- alt.Chart(bubble_data, title="Distance per Day (Bubble Chart)")
39
- .mark_circle()
40
- .encode(
41
- x=alt.X("date:T", title="Date"),
42
- y=alt.Y("distance:Q", title="Distance (km)"),
43
- size=alt.Size(
44
- "distance:Q", scale=alt.Scale(range=[10, 300]), title="Distance"
45
- ),
46
- color=alt.Color("activity:N", title="Activity"),
47
- tooltip=[
48
- alt.Tooltip("date:T", title="Date"),
49
- alt.Tooltip("distance:Q", title="Distance (km)", format=".1f"),
50
- alt.Tooltip("activity:N", title="Activity"),
51
- alt.Tooltip("activity_url:N", title="Activity Link"),
52
- ],
53
- )
54
- .properties(height=800, width=1200)
55
- .interactive()
56
- .to_json(format="vega")
45
+ return render_template(
46
+ "bubble_chart/index.html.j2",
47
+ bubble_chart_distance=_make_bubble_chart(bubble_data, column_distance),
48
+ bubble_chart_elevation_gain=_make_bubble_chart(
49
+ bubble_data, column_elevation_gain
50
+ ),
57
51
  )
58
52
 
59
- return render_template("bubble_chart/index.html.j2", bubble_chart=bubble_chart)
60
-
61
53
  return blueprint
54
+
55
+
56
+ def _make_bubble_chart(bubble_data, column: ColumnDescription):
57
+ return (
58
+ alt.Chart(bubble_data, title=f"{column.display_name} per Day (Bubble Chart)")
59
+ .mark_circle()
60
+ .encode(
61
+ x=alt.X("date:T", title="Date"),
62
+ y=alt.Y(
63
+ f"{column.name}:Q",
64
+ title=f"{column.display_name} ({column.unit})",
65
+ ),
66
+ size=alt.Size(
67
+ f"{column.name}:Q",
68
+ scale=alt.Scale(range=[10, 300]),
69
+ title=f"{column.display_name}",
70
+ ),
71
+ color=alt.Color("activity:N", title="Activity"),
72
+ tooltip=[
73
+ alt.Tooltip("date:T", title="Date"),
74
+ alt.Tooltip(
75
+ f"{column.name}:Q",
76
+ title=f"{column.display_name} ({column.unit})",
77
+ format=column.format,
78
+ ),
79
+ alt.Tooltip("activity:N", title="Activity"),
80
+ alt.Tooltip("activity_url:N", title="Activity Link"),
81
+ ],
82
+ )
83
+ .properties(height=800, width=1200)
84
+ .interactive()
85
+ .to_json(format="vega")
86
+ )
@@ -1,10 +1,14 @@
1
1
  import collections
2
2
  import datetime
3
3
 
4
+ import pandas as pd
5
+ import sqlalchemy
4
6
  from flask import Blueprint
5
7
  from flask import render_template
6
8
 
7
9
  from ...core.activities import ActivityRepository
10
+ from ...core.datamodel import Activity
11
+ from ...core.datamodel import DB
8
12
 
9
13
 
10
14
  def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
@@ -12,9 +16,14 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
12
16
 
13
17
  @blueprint.route("/")
14
18
  def index():
15
- meta = repository.meta
19
+ data = DB.session.execute(
20
+ sqlalchemy.select(Activity.start, Activity.distance_km)
21
+ ).all()
22
+ df = pd.DataFrame(data)
23
+ df["year"] = df["start"].dt.year
24
+ df["month"] = df["start"].dt.month
16
25
 
17
- monthly_distance = meta.groupby(
26
+ monthly_distance = df.groupby(
18
27
  ["year", "month"],
19
28
  ).apply(lambda group: sum(group["distance_km"]), include_groups=False)
20
29
  monthly_distance.name = "total_distance_km"
@@ -24,7 +33,7 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
24
33
  .fillna(0.0)
25
34
  )
26
35
 
27
- yearly_distance = meta.groupby(["year"]).apply(
36
+ yearly_distance = df.groupby(["year"]).apply(
28
37
  lambda group: sum(group["distance_km"]), include_groups=False
29
38
  )
30
39
  yearly_distance.name = "total_distance_km"
@@ -34,7 +43,6 @@ def make_calendar_blueprint(repository: ActivityRepository) -> Blueprint:
34
43
  }
35
44
 
36
45
  context = {
37
- "num_activities": len(repository),
38
46
  "monthly_distances": monthly_pivot,
39
47
  "yearly_distances": yearly_distances,
40
48
  }