API & Client: chart with month activities on dashboard - #9 (WIP)
This commit is contained in:
parent
0128985664
commit
30f112c094
@ -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:
|
||||||
|
@ -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':
|
||||||
{
|
{
|
||||||
|
13
mpwo_client/src/actions/stats.js
Normal file
13
mpwo_client/src/actions/stats.js
Normal 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}`)))
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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 />
|
||||||
|
25
mpwo_client/src/mwpoApi/stats.js
Normal file
25
mpwo_client/src/mwpoApi/stats.js
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -53,5 +53,8 @@ export default {
|
|||||||
},
|
},
|
||||||
sports: {
|
sports: {
|
||||||
...emptyData,
|
...emptyData,
|
||||||
}
|
},
|
||||||
|
statistics: {
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user