Pokus o fanouškovský web na téma formule jedna, s využitím volně dostupného kódu pro live tracking závodů
Dva zdroje dat, dva různé účely
Jolpica-F1 mi dává klasiku: kalendář, standings a výsledky. OpenF1 používám tam, kde je potřeba telemetry, třeba GPS body tratě nebo průběžné pozice během závodu. Dva zdroje, dvě různé práce.
Nejdřív potřebuju říct, odkud data vůbec jsou. Každý zdroj odpovídá na jinou otázku a putuje jinou cestou.
Klasická data jdou rovnou do přehledů. Telemetry musí projít převodem na tvary, které frontend umí nakreslit, a právě tenhle převod je nejzajímavější část projektu.
Proč vlastní klient pro Jolpica
def _get(path: str) -> dict:
if not path.endswith("/"):
path += "/"
resp = requests.get(
f"{BASE_URL}/{path}",
headers={"User-Agent": USER_AGENT},
timeout=REQUEST_TIMEOUT,
)
resp.raise_for_status()
return resp.json()["MRData"]
def get_schedule(season: int) -> list:
"""List of races for a season: round, name, date/time, circuit info."""
data = _get(f"{season}")
races = data.get("RaceTable", {}).get("Races", [])
Trať vzniká z telemetry
def _decimate(points: list, max_points: int = MAX_POINTS) -> list:
if len(points) <= max_points:
return points
step = len(points) / max_points
return [points[int(i * step)] for i in range(max_points)]
def _points_to_path(points: list) -> dict:
xs = [p["x"] for p in points]
ys = [p["y"] for p in points]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
span_x = max(max_x - min_x, 1)
span_y = max(max_y - min_y, 1)
avail_w = VIEWBOX_W - 2 * PADDING
avail_h = VIEWBOX_H - 2 * PADDING
scale = min(avail_w / span_x, avail_h / span_y)
Live Race Tracker pracuje s reálnou časovou osou
def build_position_timeline_sync(circuit_key: int, year: int) -> Optional[dict]:
session = _find_race_session(circuit_key, year)
if not session:
return None
raw = _get("position", session_key=session["session_key"])
raw = [p for p in raw if p.get("position") is not None and p.get("driver_number") is not None]
if not raw:
return None
raw.sort(key=lambda p: p["date"])
start = datetime.fromisoformat(raw[0]["date"])
events = [
{
"t": round((datetime.fromisoformat(p["date"]) - start).total_seconds(), 1),
"driver_number": p["driver_number"],
"position": p["position"],
}
for p in raw
]
Cache není jen optimalizace
Server má obecnou `_cached` vrstvu pro data z Jolpiky. Kalendář se klidně drží dlouho, standings kratší dobu a finální pořadí celý týden. Když živé stažení selže, server sáhne po starší cache. Prostě záchranná síť pro případ, že externí služba vypadne.
SVG místo obrázků
Tratě jsou SVG path, takže se škálují bez ztráty kvality a nemusím řešit obrázky. Backend umí použít i předpřipravené layouty z repozitáře, takže u známých závodů nemusí pokaždé sahat ven.
Co bylo technicky zajímavé
Nejzajímavější je převod cizích dat na něco, co UI opravdu chce. Není to jen fetch: musíš rozhodnout, co se cachuje, jak se to normalizuje, co se stane při výpadku API a jak z hromady telemetry vyrobit jednu malou čistou trať.
Rozšíření
Architektura je připravená na další vizualizace závodů, standings a vlastnosti tratí. Největší prostor je v dalších datových vrstvách, ne v přidávání náhodných UI komponent.
Z čeho je projekt postavený
Žádná technologie tu není jen pro parádu. Tady je rychlý přehled, kdo za co odpovídá.
FastAPI
Jolpica-F1
OpenF1
MongoDB cache
SVG / charts
Co je na projektu podstatné?
Nejde mi jen o to, jak to vypadá. Baví mě hlavně to, co je pod tím: kudy data tečou, co se validuje, co může selhat a proč jsem to udělala právě takhle.
Napiš mi
