Add regional stats to homepage

This commit is contained in:
Paul Bienkowski 2022-07-25 17:47:47 +02:00 committed by gluap
parent 68c37be383
commit b12cb1a695
No known key found for this signature in database
4 changed files with 136 additions and 16 deletions

View file

@ -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)

View file

@ -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 (
<>
<Header as="h2">Top Regions</Header>
<div>
<Loader active={stats == null} />
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Region name</Table.HeaderCell>
<Table.HeaderCell>Event count</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{stats
?.slice((page - 1) * PER_PAGE, page * PER_PAGE)
?.map((area) => (
<Table.Row key={area.id}>
<Table.Cell>{area.name}</Table.Cell>
<Table.Cell>{area.overtaking_event_count}</Table.Cell>
</Table.Row>
))}
</Table.Body>
{pageCount > 1 && <Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan="2">
<Pagination
floated="right"
activePage={page}
totalPages={pageCount}
onPageChange={(e, data) => setPage(data.activePage as number)}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>}
</Table>
</div>
</>
);
}

View file

@ -1,4 +1,5 @@
export {default as Avatar} from './Avatar'
export {default as Chart} from './Chart'
export {default as ColorMapLegend, DiscreteColorMapLegend} from './ColorMapLegend'
export {default as FileDrop} from './FileDrop'
export {default as FileUploadField} from './FileUploadField'
@ -6,7 +7,7 @@ export {default as FormattedDate} from './FormattedDate'
export {default as LoginButton} from './LoginButton'
export {default as Map} from './Map'
export {default as Page} from './Page'
export {default as RegionStats} from './RegionStats'
export {default as Stats} from './Stats'
export {default as StripMarkdown} from './StripMarkdown'
export {default as Chart} from './Chart'
export {default as Visibility} from './Visibility'

View file

@ -1,28 +1,28 @@
import React from 'react'
import {Grid, Loader, Header, Item} from 'semantic-ui-react'
import {Link} from 'react-router-dom'
import {Message, Grid, Loader, Header, Item} from 'semantic-ui-react'
import {useObservable} from 'rxjs-hooks'
import {of, from} from 'rxjs'
import {map, switchMap} from 'rxjs/operators'
import {useTranslation} from 'react-i18next'
import api from 'api'
import {Stats, Page} from 'components'
import type {Track} from 'types'
import {Stats, Page, Map} from 'components'
import {TrackListItem, NoPublicTracksMessage} from './TracksPage'
import {TrackListItem} from './TracksPage'
import styles from './HomePage.module.less'
function MostRecentTrack() {
const {t} = useTranslation()
const track: Track | null = useObservable(
const tracks: Track[] | null = useObservable(
() =>
of(null).pipe(
switchMap(() => from(api.fetch('/tracks?limit=1'))),
map((response) => response?.tracks?.[0])
switchMap(() => from(api.fetch("/tracks?limit=3"))),
map((response) => response?.tracks)
),
null,
[]
)
);
return (
<>
@ -32,11 +32,13 @@ function MostRecentTrack() {
<NoPublicTracksMessage />
) : track ? (
<Item.Group>
<TrackListItem track={track} />
{tracks.map((track) => (
<TrackListItem key={track.id} track={track} />
))}
</Item.Group>
) : null}
</>
)
);
}
export default function HomePage() {
@ -46,12 +48,13 @@ export default function HomePage() {
<Grid.Row>
<Grid.Column width={8}>
<Stats />
<MostRecentTrack />
</Grid.Column>
<Grid.Column width={8}>
<MostRecentTrack />
<RegionStats />
</Grid.Column>
</Grid.Row>
</Grid>
</Page>
)
);
}