Add regional stats to homepage

This commit is contained in:
Paul Bienkowski 2022-07-25 17:47:47 +02:00
parent e38bc9bd76
commit fac793e3a0
4 changed files with 145 additions and 25 deletions

View file

@ -4,12 +4,12 @@ from typing import Optional
from operator import and_ from operator import and_
from functools import reduce from functools import reduce
from sqlalchemy import select, func from sqlalchemy import select, func, desc
from sanic.response import json from sanic.response import json
from obs.api.app import api 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 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 Avatar} from './Avatar'
export {default as Chart} from './Chart'
export {default as ColorMapLegend, DiscreteColorMapLegend} from './ColorMapLegend' export {default as ColorMapLegend, DiscreteColorMapLegend} from './ColorMapLegend'
export {default as FileDrop} from './FileDrop' export {default as FileDrop} from './FileDrop'
export {default as FileUploadField} from './FileUploadField' export {default as FileUploadField} from './FileUploadField'
@ -6,6 +7,6 @@ export {default as FormattedDate} from './FormattedDate'
export {default as LoginButton} from './LoginButton' export {default as LoginButton} from './LoginButton'
export {default as Map} from './Map' export {default as Map} from './Map'
export {default as Page} from './Page' export {default as Page} from './Page'
export {default as RegionStats} from './RegionStats'
export {default as Stats} from './Stats' export {default as Stats} from './Stats'
export {default as StripMarkdown} from './StripMarkdown' export {default as StripMarkdown} from './StripMarkdown'
export {default as Chart} from './Chart'

View file

@ -1,42 +1,44 @@
import React from 'react' import React from "react";
import {Link} from 'react-router-dom' import { Link } from "react-router-dom";
import {Message, Grid, Loader, Header, Item} from 'semantic-ui-react' import { Message, Grid, Loader, Header, Item } from "semantic-ui-react";
import {useObservable} from 'rxjs-hooks' import { useObservable } from "rxjs-hooks";
import {of, from} from 'rxjs' import { of, from } from "rxjs";
import {map, switchMap} from 'rxjs/operators' import { map, switchMap } from "rxjs/operators";
import api from 'api' import api from "api";
import {Stats, Page, Map} from 'components' import { RegionStats, Stats, Page, Map } from "components";
import {TrackListItem} from './TracksPage' import { TrackListItem } from "./TracksPage";
import styles from './HomePage.module.less' import styles from "./HomePage.module.less";
function MostRecentTrack() { function MostRecentTrack() {
const track: Track | null = useObservable( const tracks: Track[] | null = useObservable(
() => () =>
of(null).pipe( of(null).pipe(
switchMap(() => from(api.fetch('/tracks?limit=1'))), switchMap(() => from(api.fetch("/tracks?limit=3"))),
map((response) => response?.tracks?.[0]) map((response) => response?.tracks)
), ),
null, null,
[] []
) );
return ( return (
<> <>
<Header as="h2">Most recent track</Header> <Header as="h2">Most recent tracks</Header>
<Loader active={track === null} /> <Loader active={tracks === null} />
{track === undefined ? ( {tracks?.length === 0 ? (
<Message> <Message>
No public tracks yet. <Link to="/upload">Upload the first!</Link> No public tracks yet. <Link to="/upload">Upload the first!</Link>
</Message> </Message>
) : track ? ( ) : tracks ? (
<Item.Group> <Item.Group>
<TrackListItem track={track} /> {tracks.map((track) => (
<TrackListItem key={track.id} track={track} />
))}
</Item.Group> </Item.Group>
) : null} ) : null}
</> </>
) );
} }
export default function HomePage() { export default function HomePage() {
@ -46,12 +48,13 @@ export default function HomePage() {
<Grid.Row> <Grid.Row>
<Grid.Column width={8}> <Grid.Column width={8}>
<Stats /> <Stats />
<MostRecentTrack />
</Grid.Column> </Grid.Column>
<Grid.Column width={8}> <Grid.Column width={8}>
<MostRecentTrack /> <RegionStats />
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>
</Grid> </Grid>
</Page> </Page>
) );
} }