Add date range to statistics

This commit is contained in:
Paul Bienkowski 2021-05-14 20:16:05 +02:00
parent 420b4f2a85
commit b34fbb1ee7
2 changed files with 110 additions and 42 deletions

View file

@ -1,5 +1,7 @@
const router = require('express').Router();
const mongoose = require('mongoose');
const { DateTime } = require('luxon');
const Track = mongoose.model('Track');
const User = mongoose.model('User');
const wrapRoute = require('../../_helpers/wrapRoute');
@ -13,31 +15,50 @@ const TRACK_DURATION_ROUNDING = 120;
router.get(
'/',
wrapRoute(async (req, res) => {
const trackCount = await Track.find().count();
const publicTrackCount = await Track.find({ public: true }).count();
const userCount = await User.find().count();
const start = DateTime.fromISO(req.query.start);
const end = DateTime.fromISO(req.query.end);
const dateFilter = {
$ne: null,
...(start.isValid ? { $gte: start.toJSDate() } : {}),
...(end.isValid ? { $lt: end.toJSDate() } : {}),
};
const trackCount = await Track.find({
'statistics.recordedAt': dateFilter,
}).count();
const publicTrackCount = await Track.find({
'statistics.recordedAt': dateFilter,
public: true,
}).count();
const userCount = await User.find({
createdAt: dateFilter,
}).count();
const trackStats = await Track.aggregate([
{
$match: {
'statistics.recordedAt': dateFilter,
},
},
{
$addFields: {
trackLength: {
$cond: [
{$lt: ['$statistics.length', 500000]},
'$statistics.length',
0,
],
$cond: [{ $lt: ['$statistics.length', 500000] }, '$statistics.length', 0],
},
numEvents: '$statistics.numEvents',
trackDuration: {
$cond: [
{ $and: ['$statistics.recordedUntil', '$statistics.recordedAt', {$gt: ['$statistics.recordedAt', new Date('2010-01-01')]}] },
{ $and: ['$statistics.recordedUntil', { $gt: ['$statistics.recordedAt', new Date('2010-01-01')] }] },
{ $subtract: ['$statistics.recordedUntil', '$statistics.recordedAt'] },
0,
],
},
},
},
{ $project: {trackLength: true, numEvents: true, trackDuration: true } },
{ $project: { trackLength: true, numEvents: true, trackDuration: true } },
{
$group: {
_id: 'sum',
@ -48,12 +69,14 @@ router.get(
},
]);
const [trackLength, numEvents, trackDuration] = trackStats.length > 0
? [trackStats[0].trackLength, trackStats[0].numEvents, trackStats[0].trackDuration]
: [0,0,0];
const [trackLength, numEvents, trackDuration] =
trackStats.length > 0
? [trackStats[0].trackLength, trackStats[0].numEvents, trackStats[0].trackDuration]
: [0, 0, 0];
const trackLengthPrivatized = Math.floor(trackLength / TRACK_LENGTH_ROUNDING) * TRACK_LENGTH_ROUNDING;
const trackDurationPrivatized = Math.round(trackDuration / 1000 / TRACK_DURATION_ROUNDING) * TRACK_DURATION_ROUNDING;
const trackDurationPrivatized =
Math.round(trackDuration / 1000 / TRACK_DURATION_ROUNDING) * TRACK_DURATION_ROUNDING;
return res.json({
publicTrackCount,

View file

@ -1,11 +1,11 @@
import React from 'react'
import React, {useState, useCallback} from 'react'
import {Link} from 'react-router-dom'
import {Message, Grid, Loader, Statistic, Segment, Header, Item} from 'semantic-ui-react'
import {Message, Grid, Loader, Statistic, Segment, Header, Item, Menu} from 'semantic-ui-react'
import {useObservable} from 'rxjs-hooks'
import {of, from} from 'rxjs'
import {map, switchMap} from 'rxjs/operators'
import {of, from, concat} from 'rxjs'
import {tap, map, switchMap, distinctUntilChanged} from 'rxjs/operators'
import {fromLonLat} from 'ol/proj'
import {Duration} from 'luxon'
import {Duration, DateTime} from 'luxon'
import api from '../api'
import {Map, Page, RoadsLayer} from '../components'
@ -16,7 +16,7 @@ import styles from './HomePage.module.scss'
function formatDuration(seconds) {
return Duration.fromMillis((seconds ?? 0) * 1000)
.as('hours')
.toFixed(1)
.toFixed(1) + ' h'
}
function WelcomeMap() {
@ -36,34 +36,79 @@ function WelcomeMap() {
}
function Stats() {
const stats = useObservable(() => of(null).pipe(switchMap(() => api.fetch('/stats'))))
const [timeframe, setTimeframe] = useState('all_time')
const onClick = useCallback((_e, {name}) => setTimeframe(name), [setTimeframe])
const stats = useObservable(
(_$, inputs$) =>
inputs$.pipe(
map((inputs) => inputs[0]),
distinctUntilChanged(),
map((timeframe_) => {
const now = DateTime.now()
switch (timeframe_) {
case 'this_month':
return {
start: now.startOf('month').toISODate(),
end: now.endOf('month').toISODate(),
}
case 'this_year':
return {
start: now.startOf('year').toISODate(),
end: now.endOf('year').toISODate(),
}
case 'all_time':
default:
return {}
}
}),
switchMap((query) => concat(of(null), from(api.get('/stats', {query})))),
tap(console.log),
),
null,
[timeframe]
)
return (
<>
<Header as="h2">Statistics</Header>
<Segment>
<Loader active={stats == null} />
<div>
<Segment attached="top">
<Loader active={stats == null} />
<Statistic.Group widths={2} size="tiny">
<Statistic>
<Statistic.Value>{stats ? `${Number(stats?.trackLength / 1000).toFixed(1)} km` : '...'}</Statistic.Value>
<Statistic.Label>Total track length</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{stats ? formatDuration(stats?.trackDuration) : '...'}</Statistic.Value>
<Statistic.Label>Time recorded</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{stats?.numEvents ?? '...'}</Statistic.Value>
<Statistic.Label>Events confirmed</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{stats?.userCount ?? '...'}</Statistic.Value>
<Statistic.Label>Members joined</Statistic.Label>
</Statistic>
</Statistic.Group>
</Segment>
<Statistic.Group widths={2} size="tiny">
<Statistic>
<Statistic.Value>{Number(stats?.trackLength / 1000).toFixed(1)} km</Statistic.Value>
<Statistic.Label>Total track length</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{formatDuration(stats?.trackDuration)} h</Statistic.Value>
<Statistic.Label>Time recorded</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{stats?.numEvents}</Statistic.Value>
<Statistic.Label>Events confirmed</Statistic.Label>
</Statistic>
<Statistic>
<Statistic.Value>{stats?.userCount}</Statistic.Value>
<Statistic.Label>Members joined</Statistic.Label>
</Statistic>
</Statistic.Group>
</Segment>
<Menu widths={3} attached="bottom" size="small">
<Menu.Item name="this_month" active={timeframe === 'this_month'} onClick={onClick}>
This month
</Menu.Item>
<Menu.Item name="this_year" active={timeframe === 'this_year'} onClick={onClick}>
This year
</Menu.Item>
<Menu.Item name="all_time" active={timeframe === 'all_time'} onClick={onClick}>
All time
</Menu.Item>
</Menu>
</div>
</>
)
}