"""Synthetic Strava-style activities for demos and local testing. Each demo profile is designed to trip a *different* signal in the analytics engine, so the demo dropdown doubles as a tour of what the engine detects: excelling -> increasing_consistency + improving pace, safe build stagnating -> flat mileage & pace -> pace_trend "plateauing" returning_after_break -> ~2-week gap at the end -> "returning_after_break" Dates are relative to "now" and built fresh on every call (via the builder functions below), so the runs always land in the right rolling-week buckets no matter how long the app has been running. """ from datetime import datetime, timedelta def _iso(days_ago: float, hour: int = 8) -> str: d = (datetime.now() - timedelta(days=days_ago)).replace( hour=hour, minute=0, second=0, microsecond=0 ) return d.isoformat() def _excelling() -> list[dict]: """Consistent runner on a healthy upward trend over ~8 weeks. Mileage grows ~8% week-over-week (under the 10% spike threshold), pace steadily improves, and run count ticks up this week (3 -> 4). """ return [ # This week (0–6d): 4 runs, ~19.5 km, ~4:55/km {"type": "Run", "distance": 5000, "moving_time": 1475, "start_date": _iso(1)}, {"type": "Run", "distance": 5500, "moving_time": 1620, "start_date": _iso(3)}, {"type": "Run", "distance": 4000, "moving_time": 1180, "start_date": _iso(5)}, {"type": "Run", "distance": 5000, "moving_time": 1475, "start_date": _iso(6)}, # Last week (7–13d): 3 runs, ~18 km, ~5:05/km {"type": "Run", "distance": 6000, "moving_time": 1830, "start_date": _iso(8)}, {"type": "Run", "distance": 6000, "moving_time": 1830, "start_date": _iso(10)}, {"type": "Run", "distance": 6000, "moving_time": 1830, "start_date": _iso(12)}, # 2 weeks ago (14–20d): 3 runs, ~16.5 km, ~5:15/km {"type": "Run", "distance": 5500, "moving_time": 1733, "start_date": _iso(15)}, {"type": "Run", "distance": 5500, "moving_time": 1733, "start_date": _iso(17)}, {"type": "Run", "distance": 5500, "moving_time": 1733, "start_date": _iso(19)}, # 3 weeks ago (21–27d): 3 runs, ~15 km, ~5:25/km {"type": "Run", "distance": 5000, "moving_time": 1625, "start_date": _iso(22)}, {"type": "Run", "distance": 5000, "moving_time": 1625, "start_date": _iso(24)}, {"type": "Run", "distance": 5000, "moving_time": 1625, "start_date": _iso(26)}, # 4 weeks ago (28–34d): 2 runs, ~13 km, ~5:35/km {"type": "Run", "distance": 6500, "moving_time": 2178, "start_date": _iso(29)}, {"type": "Run", "distance": 6500, "moving_time": 2178, "start_date": _iso(32)}, # 5 weeks ago (35–41d): 2 runs, ~12 km, ~5:40/km {"type": "Run", "distance": 6000, "moving_time": 2040, "start_date": _iso(36)}, {"type": "Run", "distance": 6000, "moving_time": 2040, "start_date": _iso(39)}, # 6 weeks ago (42–48d): 2 runs, ~10 km, ~5:45/km {"type": "Run", "distance": 5000, "moving_time": 1725, "start_date": _iso(43)}, {"type": "Run", "distance": 5000, "moving_time": 1725, "start_date": _iso(46)}, # 7 weeks ago (49–55d): 1 run, ~8 km, ~5:50/km {"type": "Run", "distance": 8000, "moving_time": 2800, "start_date": _iso(51)}, # --- junk that must be filtered out --- {"type": "Run", "distance": 40, "moving_time": 9, "start_date": _iso(2)}, {"type": "Ride", "distance": 24000, "moving_time": 3600, "start_date": _iso(4)}, ] def _stagnating() -> list[dict]: """Reliable but stuck: same mileage and pace every week for a month. Trends: mileage change ~0%, pace flat -> "plateauing", no growth signals. The story the coach should tell is 'add stimulus to break the plateau'. """ runs = [] # 8 weeks, 3 runs each, every run 5 km at exactly 5:20/km. for week in range(8): base = week * 7 for offset in (1, 3, 5): runs.append( {"type": "Run", "distance": 5000, "moving_time": 1600, "start_date": _iso(base + offset)} ) # a little junk for realism runs.append({"type": "Ride", "distance": 18000, "moving_time": 3000, "start_date": _iso(4)}) return runs def _returning_after_break() -> list[dict]: """Trained well, then took ~2 weeks off — last run was 16 days ago. days_since_last_run > 10 -> "returning_after_break". This and last week are empty, so the chart shows the drop-off and the coach should ease them back in. """ return [ # This week & last week (0–13d): EMPTY — the break. # 2 weeks ago (14–20d): last runs before the break {"type": "Run", "distance": 5000, "moving_time": 1600, "start_date": _iso(16)}, {"type": "Run", "distance": 6000, "moving_time": 1980, "start_date": _iso(18)}, {"type": "Run", "distance": 5000, "moving_time": 1650, "start_date": _iso(20)}, # 3 weeks ago (21–27d): a solid block {"type": "Run", "distance": 6000, "moving_time": 2040, "start_date": _iso(23)}, {"type": "Run", "distance": 7000, "moving_time": 2380, "start_date": _iso(25)}, {"type": "Run", "distance": 5000, "moving_time": 1700, "start_date": _iso(27)}, # 4 weeks ago (28–34d) {"type": "Run", "distance": 6000, "moving_time": 2010, "start_date": _iso(29)}, {"type": "Run", "distance": 6000, "moving_time": 2010, "start_date": _iso(31)}, {"type": "Run", "distance": 7000, "moving_time": 2345, "start_date": _iso(33)}, # 5 weeks ago (35–41d) {"type": "Run", "distance": 5000, "moving_time": 1700, "start_date": _iso(36)}, {"type": "Run", "distance": 6000, "moving_time": 2040, "start_date": _iso(38)}, # 6 weeks ago (42–48d) {"type": "Run", "distance": 6000, "moving_time": 2010, "start_date": _iso(43)}, {"type": "Run", "distance": 6000, "moving_time": 2010, "start_date": _iso(45)}, {"type": "Run", "distance": 5000, "moving_time": 1700, "start_date": _iso(47)}, # 7 weeks ago (49–55d) {"type": "Run", "distance": 5000, "moving_time": 1725, "start_date": _iso(51)}, {"type": "Run", "distance": 6000, "moving_time": 2070, "start_date": _iso(54)}, # --- junk that must be filtered out --- {"type": "Run", "distance": 25, "moving_time": 6, "start_date": _iso(17)}, ] _DEMOS = { "excelling": _excelling, "stagnating": _stagnating, "returning_after_break": _returning_after_break, } # Keep the dropdown choices and the dispatch in sync from one source of truth. DEMO_GROUPS = tuple(_DEMOS) def demo_activities(demo: str = "excelling") -> list[dict]: """Return a fresh synthetic activity list for the chosen demo profile.""" builder = _DEMOS.get(demo, _excelling) return builder()