API & Client: chart with month activities on dashboard - #9 (WIP)

This commit is contained in:
Sam 2018-06-06 20:17:37 +02:00
parent 0128985664
commit 30f112c094
11 changed files with 235 additions and 36 deletions

View File

@ -70,14 +70,12 @@ def get_activities(user_id, type):
activity_date = activity.activity_date - timedelta( activity_date = activity.activity_date - timedelta(
days=activity.activity_date.isoweekday() days=activity.activity_date.isoweekday()
) )
time_period = datetime.strftime(activity_date, time_period = datetime.strftime(activity_date, "%Y-%m-%d")
"%Y-%m-%d_W%U")
elif time == 'weekm': # week start Monday elif time == 'weekm': # week start Monday
activity_date = activity.activity_date - timedelta( activity_date = activity.activity_date - timedelta(
days=activity.activity_date.weekday() days=activity.activity_date.weekday()
) )
time_period = datetime.strftime(activity_date, time_period = datetime.strftime(activity_date, "%Y-%m-%d")
"%Y-%m-%d_W%W")
elif time == 'month': elif time == 'month':
time_period = datetime.strftime(activity.activity_date, "%Y-%m") # noqa time_period = datetime.strftime(activity.activity_date, "%Y-%m") # noqa
elif time == 'year' or not time: elif time == 'year' or not time:

View File

@ -468,7 +468,7 @@ def test_get_stats_by_week_all_activities(
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['statistics'] == \ assert data['data']['statistics'] == \
{ {
'2017-03-19_W12': '2017-03-19':
{ {
'1': '1':
{ {
@ -477,7 +477,7 @@ def test_get_stats_by_week_all_activities(
'total_duration': 1024 'total_duration': 1024
} }
}, },
'2017-05-28_W22': '2017-05-28':
{ {
'1': '1':
{ {
@ -486,7 +486,7 @@ def test_get_stats_by_week_all_activities(
'total_duration': 3456 'total_duration': 3456
} }
}, },
'2017-12-31_W53': '2017-12-31':
{ {
'1': '1':
{ {
@ -495,7 +495,7 @@ def test_get_stats_by_week_all_activities(
'total_duration': 1024 'total_duration': 1024
} }
}, },
'2018-02-18_W07': '2018-02-18':
{ {
'1': '1':
{ {
@ -504,7 +504,7 @@ def test_get_stats_by_week_all_activities(
'total_duration': 1600 'total_duration': 1600
} }
}, },
'2018-03-25_W12': '2018-03-25':
{ {
'1': '1':
{ {
@ -519,7 +519,7 @@ def test_get_stats_by_week_all_activities(
'total_duration': 6000 'total_duration': 6000
} }
}, },
'2018-05-06_W18': '2018-05-06':
{ {
'1': '1':
{ {
@ -558,7 +558,7 @@ def test_get_stats_by_week_all_activities_week_13(
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['statistics'] == \ assert data['data']['statistics'] == \
{ {
'2018-03-25_W12': '2018-03-25':
{ {
'1': '1':
{ {
@ -603,7 +603,7 @@ def test_get_stats_by_weekm_all_activities(
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['statistics'] == \ assert data['data']['statistics'] == \
{ {
'2017-03-20_W12': '2017-03-20':
{ {
'1': '1':
{ {
@ -612,7 +612,7 @@ def test_get_stats_by_weekm_all_activities(
'total_duration': 1024 'total_duration': 1024
} }
}, },
'2017-05-29_W22': '2017-05-29':
{ {
'1': '1':
{ {
@ -621,7 +621,7 @@ def test_get_stats_by_weekm_all_activities(
'total_duration': 3456 'total_duration': 3456
} }
}, },
'2018-01-01_W01': '2018-01-01':
{ {
'1': '1':
{ {
@ -630,7 +630,7 @@ def test_get_stats_by_weekm_all_activities(
'total_duration': 1024 'total_duration': 1024
} }
}, },
'2018-02-19_W08': '2018-02-19':
{ {
'1': '1':
{ {
@ -639,7 +639,7 @@ def test_get_stats_by_weekm_all_activities(
'total_duration': 1600 'total_duration': 1600
} }
}, },
'2018-03-26_W13': '2018-03-26':
{ {
'1': '1':
{ {
@ -654,7 +654,7 @@ def test_get_stats_by_weekm_all_activities(
'total_duration': 6000 'total_duration': 6000
} }
}, },
'2018-05-07_W19': '2018-05-07':
{ {
'1': '1':
{ {
@ -693,7 +693,7 @@ def test_get_stats_by_weekm_all_activities_week_13(
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['statistics'] == \ assert data['data']['statistics'] == \
{ {
'2018-03-26_W13': '2018-03-26':
{ {
'1': '1':
{ {

View File

@ -0,0 +1,13 @@
import mpwoApi from '../mwpoApi/stats'
import { setData, setError } from './index'
export const getStats = (userId, type, data) => dispatch => mpwoApi
.getStats(userId, type, data)
.then(ret => {
if (ret.status === 'success') {
dispatch(setData('statistics', ret.data))
} else {
dispatch(setError(`statistics: ${ret.message}`))
}
})
.catch(error => dispatch(setError(`statistics: ${error}`)))

View File

@ -115,6 +115,10 @@ input, textarea {
font-style: italic; font-style: italic;
} }
.chart-month {
font-size: 0.8em;
}
.chart-radio { .chart-radio {
display: flex; display: flex;
font-size: 0.8em; font-size: 0.8em;

View File

@ -1,15 +1,132 @@
import { endOfMonth, format, startOfMonth } from 'date-fns'
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import {
Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis
} from 'recharts'
import { getStats } from '../../actions/stats'
import { formatStats } from '../../utils'
export default function ActivityCard () { class Statistics extends React.Component {
constructor(props, context) {
super(props, context)
const date = new Date()
this.state = {
start: startOfMonth(date),
end: endOfMonth(date),
displayedData: 'distance'
}
}
componentDidMount() {
this.props.loadMonthActivities(
this.props.user.id,
this.state.start,
this.state.end,
)
}
handleRadioChange (changeEvent) {
this.setState({
displayedData: changeEvent.target.name
})
}
render() {
const { sports, statistics } = this.props
const { displayedData, end, start } = this.state
const data = []
const stats = formatStats(statistics, sports, start, end)
const colors = [
'#55a8a3',
'#98C3A9',
'#D0838A',
'#ECC77E',
'#926692',
'#929292'
]
return ( return (
<div className="card activity-card"> <div className="card activity-card">
<div className="card-header"> <div className="card-header">
Statistics This month
</div> </div>
<div className="card-body"> <div className="card-body chart-month">
coming soon... {data === [] ? (
'No activities'
) : (
<div>
<div className="row chart-radio">
<label className="radioLabel col">
<input
type="radio"
name="distance"
checked={displayedData === 'distance'}
onChange={e => this.handleRadioChange(e)}
/>
distance
</label>
<label className="radioLabel col">
<input
type="radio"
name="duration"
checked={displayedData === 'duration'}
onChange={e => this.handleRadioChange(e)}
/>
duration
</label>
<label className="radioLabel col">
<input
type="radio"
name="activities"
checked={displayedData === 'activities'}
onChange={e => this.handleRadioChange(e)}
/>
activities
</label>
</div>
<ResponsiveContainer height={300}>
<BarChart
data={stats[displayedData]}
>
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
{sports.map((s, i) => (
<Bar
key={s.id}
dataKey={s.label}
stackId="a"
fill={colors[i]}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
)}
</div> </div>
</div> </div>
) )
} }
}
export default connect(
state => ({
sports: state.sports.data,
statistics: state.statistics.data,
user: state.user,
}),
dispatch => ({
loadMonthActivities: (userId, start, end) => {
const dateFormat = 'YYYY-MM-DD'
const params = {
start: format(start, dateFormat),
end: format(end, dateFormat),
time: 'week'
}
dispatch(getStats(userId, 'by_time', params))
},
})
)(Statistics)

View File

@ -43,8 +43,8 @@ class DashBoard extends React.Component {
<UserStatistics user={user} /> <UserStatistics user={user} />
<div className="row"> <div className="row">
<div className="col-md-4"> <div className="col-md-4">
<Records records={records} sports={sports} />
<Statistics /> <Statistics />
<Records records={records} sports={sports} />
</div> </div>
<div className="col-md-8"> <div className="col-md-8">
<Calendar /> <Calendar />

View File

@ -0,0 +1,25 @@
import { apiUrl, createRequest } from '../utils'
export default class MpwoApi {
static getStats(userID, type, data = {}) {
let url = `${apiUrl}stats/${userID}/${type}`
if (Object.keys(data).length > 0) {
url = `${url}?${
data.start ? `&from=${data.start}` : ''
}${
data.end ? `&to=${data.end}` : ''
}${
data.time ? `&time=${data.time}` : ''
}${
data.sport_id ? `&sport_id=${data.sport_id}` : ''
}`
}
const params = {
url: url,
method: 'GET',
}
return createRequest(params)
}
}

View File

@ -195,6 +195,9 @@ const user = (state = initial.user, action) => {
} }
} }
const statistics = (state = initial.statistics, action) =>
handleDataAndError(state, 'statistics', action)
const reducers = combineReducers({ const reducers = combineReducers({
activities, activities,
calendarActivities, calendarActivities,
@ -208,6 +211,7 @@ const reducers = combineReducers({
records, records,
router: routerReducer, router: routerReducer,
sports, sports,
statistics,
user, user,
}) })

View File

@ -53,5 +53,8 @@ export default {
}, },
sports: { sports: {
...emptyData, ...emptyData,
} },
statistics: {
data: {},
},
} }

View File

@ -1,5 +1,5 @@
import togeojson from '@mapbox/togeojson' import togeojson from '@mapbox/togeojson'
import { format, parse, subHours } from 'date-fns' import { addDays, format, parse, startOfWeek, subHours } from 'date-fns'
export const apiUrl = `${process.env.REACT_APP_API_URL}/api/` export const apiUrl = `${process.env.REACT_APP_API_URL}/api/`
export const thunderforestApiKey = `${ export const thunderforestApiKey = `${
@ -106,3 +106,38 @@ export const formatChartData = chartData => {
} }
return chartData return chartData
} }
export const formatStats = (stats, sports, startDate, endDate) => {
const nbActivitiesStats = []
const distanceStats = []
const durationStats = []
for (let day = startOfWeek(startDate);
day <= endDate;
day = addDays(day, 7)
) {
const date = format(day, 'YYYY-MM-DD')
const dataNbActivities = { date }
const dataDistance = { date }
const dataDuration = { date }
if (stats[date]) {
Object.keys(stats[date]).map(sportId => {
const sportLabel = sports.filter(s => s.id === +sportId)[0].label
dataNbActivities[sportLabel] = stats[date][sportId].nb_activities
dataDistance[sportLabel] = stats[date][sportId].total_distance
dataDuration[sportLabel] = stats[date][sportId].total_duration
return null
})
}
nbActivitiesStats.push(dataNbActivities)
distanceStats.push(dataDistance)
durationStats.push(dataDuration)
}
return {
activities: nbActivitiesStats,
distance: distanceStats,
duration: durationStats
}
}