diff --git a/api/obs/api/routes/stats.py b/api/obs/api/routes/stats.py
index 8f5603c..dfdbe7c 100644
--- a/api/obs/api/routes/stats.py
+++ b/api/obs/api/routes/stats.py
@@ -4,12 +4,12 @@ from typing import Optional
from operator import and_
from functools import reduce
-from sqlalchemy import select, func
+from sqlalchemy import select, func, desc
from sanic.response import json
from obs.api.app import api
-from obs.api.db import Track, OvertakingEvent, User
+from obs.api.db import Track, OvertakingEvent, User, Region
from obs.api.utils import round_to
@@ -167,3 +167,36 @@ async def stats(req):
# });
# }),
# );
+
+
+@api.route("/stats/regions")
+async def stats(req):
+ query = (
+ select(
+ [
+ Region.relation_id.label("id"),
+ Region.name,
+ func.count(OvertakingEvent.id).label("overtaking_event_count"),
+ ]
+ )
+ .select_from(Region)
+ .join(
+ OvertakingEvent,
+ func.ST_Within(
+ func.ST_Transform(OvertakingEvent.geometry, 3857), Region.geometry
+ ),
+ )
+ .where(Region.admin_level == 6)
+ .group_by(
+ Region.relation_id,
+ Region.name,
+ Region.relation_id,
+ Region.admin_level,
+ Region.geometry,
+ )
+ .having(func.count(OvertakingEvent.id) > 0)
+ .order_by(desc("overtaking_event_count"))
+ )
+
+ regions = list(map(dict, (await req.ctx.db.execute(query)).all()))
+ return json(regions)
diff --git a/frontend/src/components/RegionStats/index.tsx b/frontend/src/components/RegionStats/index.tsx
new file mode 100644
index 0000000..5dac6b8
--- /dev/null
+++ b/frontend/src/components/RegionStats/index.tsx
@@ -0,0 +1,83 @@
+import React, { useState, useCallback } from "react";
+import { pickBy } from "lodash";
+import {
+ Loader,
+ Statistic,
+ Pagination,
+ Segment,
+ Header,
+ Menu,
+ Table,
+ Icon,
+} from "semantic-ui-react";
+import { useObservable } from "rxjs-hooks";
+import { of, from, concat, combineLatest } from "rxjs";
+import { map, switchMap, distinctUntilChanged } from "rxjs/operators";
+import { Duration, DateTime } from "luxon";
+
+import api from "api";
+
+function formatDuration(seconds) {
+ return (
+ Duration.fromMillis((seconds ?? 0) * 1000)
+ .as("hours")
+ .toFixed(1) + " h"
+ );
+}
+
+export default function Stats() {
+ const [page, setPage] = useState(1);
+ const PER_PAGE = 10;
+ const stats = useObservable(
+ () =>
+ of(null).pipe(
+ switchMap(() => concat(of(null), from(api.get("/stats/regions"))))
+ ),
+ null
+ );
+
+ const pageCount = stats ? Math.ceil(stats.length / PER_PAGE) : 1;
+
+ return (
+ <>
+