Add regional stats to homepage
This commit is contained in:
parent
e38bc9bd76
commit
fac793e3a0
|
@ -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)
|
||||
|
|
83
frontend/src/components/RegionStats/index.tsx
Normal file
83
frontend/src/components/RegionStats/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,6 +7,6 @@ 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'
|
||||
|
|
|
@ -1,42 +1,44 @@
|
|||
import React from '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 React from "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 api from 'api'
|
||||
import {Stats, Page, Map} from 'components'
|
||||
import api from "api";
|
||||
import { RegionStats, Stats, Page, Map } from "components";
|
||||
|
||||
import {TrackListItem} from './TracksPage'
|
||||
import styles from './HomePage.module.less'
|
||||
import { TrackListItem } from "./TracksPage";
|
||||
import styles from "./HomePage.module.less";
|
||||
|
||||
function MostRecentTrack() {
|
||||
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 (
|
||||
<>
|
||||
<Header as="h2">Most recent track</Header>
|
||||
<Loader active={track === null} />
|
||||
{track === undefined ? (
|
||||
<Header as="h2">Most recent tracks</Header>
|
||||
<Loader active={tracks === null} />
|
||||
{tracks?.length === 0 ? (
|
||||
<Message>
|
||||
No public tracks yet. <Link to="/upload">Upload the first!</Link>
|
||||
</Message>
|
||||
) : track ? (
|
||||
) : tracks ? (
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue