Client: display elevation and speed chart for an activity

This commit is contained in:
Sam 2018-05-28 15:33:56 +02:00
parent 8b186c2e41
commit 681ea6643d
9 changed files with 290 additions and 24 deletions

View File

@ -1,6 +1,7 @@
import mpwoGenericApi from '../mwpoApi' import mpwoGenericApi from '../mwpoApi'
import mpwoApi from '../mwpoApi/activities' import mpwoApi from '../mwpoApi/activities'
import { history } from '../index' import { history } from '../index'
import { formatChartData } from '../utils'
import { setError } from './index' import { setError } from './index'
export const pushActivities = activities => ({ export const pushActivities = activities => ({
@ -13,6 +14,11 @@ export const setGpx = gpxContent => ({
gpxContent, gpxContent,
}) })
export const setChartData = chartData => ({
type: 'SET_CHART_DATA',
chartData,
})
export const addActivity = form => dispatch => mpwoApi export const addActivity = form => dispatch => mpwoApi
.addActivity(form) .addActivity(form)
.then(ret => { .then(ret => {
@ -54,6 +60,23 @@ export const getActivityGpx = activityId => dispatch => {
} }
export const getActivityChartData = activityId => dispatch => {
if (activityId) {
return mpwoApi
.getActivityChartData(activityId)
.then(ret => {
if (ret.status === 'success') {
dispatch(setChartData(formatChartData(ret.data.chart_data)))
} else {
dispatch(setError(`activities: ${ret.message}`))
}
})
.catch(error => dispatch(setError(`activities: ${error}`)))
}
dispatch(setChartData(null))
}
export const deleteActivity = id => dispatch => mpwoGenericApi export const deleteActivity = id => dispatch => mpwoGenericApi
.deleteData('activities', id) .deleteData('activities', id)
.then(ret => { .then(ret => {

View File

@ -0,0 +1,84 @@
import { format } from 'date-fns'
import React from 'react'
import { connect } from 'react-redux'
import {
Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
} from 'recharts'
import { getActivityChartData } from '../../../actions/activities'
class ActivityCharts extends React.Component {
componentDidMount() {
this.props.loadActivityData(this.props.activity.id)
}
componentDidUpdate(prevProps) {
if (prevProps.activity.id !==
this.props.activity.id) {
this.props.loadActivityData(this.props.activity.id)
}
}
componentWillUnmount() {
this.props.loadActivityData(null)
}
render() {
const { chartData } = this.props
return (
<div>
<ResponsiveContainer height={300}>
<LineChart
data={chartData}
margin={{ top: 15, right: 30, left: 20, bottom: 15 }}
>
<XAxis
dataKey="duration"
label={{ value: 'duration', offset: 0, position: 'bottom' }}
scale="time"
tickFormatter={time => format(time, 'HH:mm:ss')}
type="number"
/>
<YAxis
label={{ value: 'speed (km/h)', angle: -90, position: 'left' }}
yAxisId="left"
/>
<YAxis
label={{ value: 'altitude (m)', angle: -90, position: 'right' }}
yAxisId="right" orientation="right"
/>
<Line
yAxisId="left"
type="linear"
dataKey="speed"
stroke="#8884d8"
dot={false}
/>
<Line
yAxisId="right"
type="linear"
dataKey="elevation"
stroke="#808080"
dot={false}
/>
<Tooltip
labelFormatter={time => format(time, 'HH:mm:ss')}
/>
</LineChart>
</ResponsiveContainer>
</div>
)
}
}
export default connect(
state => ({
chartData: state.chartData
}),
dispatch => ({
loadActivityData: activityId => {
dispatch(getActivityChartData(activityId))
},
})
)(ActivityCharts)

View File

@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import ActivityCardHeader from './ActivityCardHeader' import ActivityCardHeader from './ActivityCardHeader'
import ActivityCharts from './ActivityCharts'
import ActivityDetails from './ActivityDetails' import ActivityDetails from './ActivityDetails'
import ActivityMap from './ActivityMap' import ActivityMap from './ActivityMap'
import CustomModal from './../../Others/CustomModal' import CustomModal from './../../Others/CustomModal'
@ -61,6 +62,7 @@ class ActivityDisplay extends React.Component {
close={() => this.displayModal(false)} close={() => this.displayModal(false)}
/>} />}
{activity && sport && activities.length === 1 && ( {activity && sport && activities.length === 1 && (
<div>
<div className="row"> <div className="row">
<div className="col"> <div className="col">
<div className="card"> <div className="card">
@ -87,6 +89,20 @@ class ActivityDisplay extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div className="row">
<div className="col">
<div className="card">
<div className="card-body">
<div className="row">
<div className="col">
<ActivityCharts activity={activity} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)} )}
</div> </div>
)} )}

View File

@ -29,4 +29,12 @@ export default class MpwoApi {
return createRequest(params) return createRequest(params)
} }
static getActivityChartData(activityId) {
const params = {
url: `${apiUrl}activities/${activityId}/chart_data`,
method: 'GET',
}
return createRequest(params)
}
} }

View File

@ -31,6 +31,15 @@ const activities = (state = initial.activities, action) => {
} }
} }
const chartData = (state = initial.chartData, action) => {
switch (action.type) {
case 'SET_CHART_DATA':
return action.chartData
default:
return state
}
}
const formData = (state = initial.formData, action) => { const formData = (state = initial.formData, action) => {
switch (action.type) { switch (action.type) {
case 'UPDATE_USER_FORMDATA': case 'UPDATE_USER_FORMDATA':
@ -163,6 +172,7 @@ const user = (state = initial.user, action) => {
const reducers = combineReducers({ const reducers = combineReducers({
activities, activities,
chartData,
formData, formData,
formProfile, formProfile,
gpx, gpx,

View File

@ -41,6 +41,7 @@ export default {
activities: { activities: {
...emptyData, ...emptyData,
}, },
chartData: [],
// check if storing gpx content is OK // check if storing gpx content is OK
gpx: null, gpx: null,
records: { records: {

View File

@ -1,5 +1,5 @@
import togeojson from '@mapbox/togeojson' import togeojson from '@mapbox/togeojson'
import { format, parse } from 'date-fns' import { format, parse, 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 = `${
@ -90,3 +90,17 @@ export const formatRecord = record => {
value: value, value: value,
} }
} }
const formatDuration = seconds => {
let newDate = new Date(0)
newDate = subHours(newDate.setSeconds(seconds), 1)
return newDate.getTime()
}
export const formatChartData = chartData => {
for (let i = 0; i < chartData.length; i++) {
chartData[i].time = new Date(chartData[i].time).getTime()
chartData[i].duration = formatDuration(chartData[i].duration)
}
return chartData
}

View File

@ -17,6 +17,7 @@
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.9", "react-router-redux": "^5.0.0-alpha.9",
"react-scripts": "1.1.4", "react-scripts": "1.1.4",
"recharts": "^1.0.0-beta.10",
"redux": "4.0.0", "redux": "4.0.0",
"redux-thunk": "^2.2.0" "redux-thunk": "^2.2.0"
}, },

117
yarn.lock
View File

@ -1870,6 +1870,10 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
clean-css@4.1.x: clean-css@4.1.x:
version "4.1.11" version "4.1.11"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a"
@ -2128,6 +2132,10 @@ copy-descriptor@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
core-js@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
core-js@^1.0.0: core-js@^1.0.0:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -2342,6 +2350,60 @@ currently-unhandled@^0.4.1:
dependencies: dependencies:
array-find-index "^1.0.1" array-find-index "^1.0.1"
d3-array@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
d3-collection@1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
d3-color@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
d3-format@1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
d3-interpolate@1, d3-interpolate@^1.1.5:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
dependencies:
d3-color "1"
d3-path@1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
d3-scale@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed"
dependencies:
d3-array "^1.2.0"
d3-collection "1"
d3-color "1"
d3-format "1"
d3-interpolate "1"
d3-time "1"
d3-time-format "2"
d3-shape@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
dependencies:
d3-path "1"
d3-time-format@2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
dependencies:
d3-time "1"
d3-time@1:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
d@1: d@1:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@ -2654,6 +2716,10 @@ dom-converter@~0.1:
dependencies: dependencies:
utila "~0.3" utila "~0.3"
dom-helpers@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
dom-serializer@0: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@ -5536,7 +5602,7 @@ lodash@4.16.4:
version "4.16.4" version "4.16.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.4.tgz#01ce306b9bad1319f2a5528674f88297aeb70127" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.4.tgz#01ce306b9bad1319f2a5528674f88297aeb70127"
"lodash@4.6.1 || ^4.16.1", "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0: "lodash@4.6.1 || ^4.16.1", "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4:
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@ -6887,7 +6953,7 @@ promisify-event@^1.0.0:
dependencies: dependencies:
pinkie-promise "^2.0.0" pinkie-promise "^2.0.0"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0: prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1:
version "15.6.1" version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies: dependencies:
@ -6985,7 +7051,7 @@ querystringify@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
raf@3.4.0: raf@3.4.0, raf@^3.2.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies: dependencies:
@ -7103,6 +7169,12 @@ react-redux@^5.0.7:
loose-envify "^1.1.0" loose-envify "^1.1.0"
prop-types "^15.6.0" prop-types "^15.6.0"
react-resize-detector@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-1.1.0.tgz#4a9831fa3caad32230478dd0185cbd2aa91a5ebf"
dependencies:
prop-types "^15.5.10"
react-router-dom@^4.2.2: react-router-dom@^4.2.2:
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
@ -7186,6 +7258,23 @@ react-side-effect@^1.1.0:
exenv "^1.2.1" exenv "^1.2.1"
shallowequal "^1.0.1" shallowequal "^1.0.1"
react-smooth@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.0.tgz#b29dbebddddb06d21b5b08962167fb9eac1897d8"
dependencies:
lodash "~4.17.4"
prop-types "^15.6.0"
raf "^3.2.0"
react-transition-group "^2.2.1"
react-transition-group@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6"
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.3.1"
prop-types "^15.6.1"
react@^16.4.0: react@^16.4.0:
version "16.4.0" version "16.4.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585"
@ -7307,6 +7396,26 @@ recast@^0.11.17:
private "~0.1.5" private "~0.1.5"
source-map "~0.5.0" source-map "~0.5.0"
recharts-scale@0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.3.2.tgz#dac7621714a4765d152cb2adbc30c73b831208c9"
recharts@^1.0.0-beta.10:
version "1.0.0-beta.10"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.0.0-beta.10.tgz#d3cd15df6b7879d5968da3c942b5fcdaf2504fe1"
dependencies:
classnames "2.2.5"
core-js "2.5.1"
d3-interpolate "^1.1.5"
d3-scale "1.0.6"
d3-shape "1.2.0"
lodash "~4.17.4"
prop-types "^15.6.0"
react-resize-detector "1.1.0"
react-smooth "1.0.0"
recharts-scale "0.3.2"
reduce-css-calc "1.3.0"
recursive-readdir@2.2.1: recursive-readdir@2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
@ -7320,7 +7429,7 @@ redent@^1.0.0:
indent-string "^2.1.0" indent-string "^2.1.0"
strip-indent "^1.0.1" strip-indent "^1.0.1"
reduce-css-calc@^1.2.6: reduce-css-calc@1.3.0, reduce-css-calc@^1.2.6:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
dependencies: dependencies: