Client - init workout chart (WIP)
This commit is contained in:
parent
146899c269
commit
c50e74143e
@ -13,6 +13,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
BarElement,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Filler,
|
||||||
|
BarController,
|
||||||
|
CategoryScale,
|
||||||
|
LineController,
|
||||||
|
LinearScale,
|
||||||
|
} from 'chart.js'
|
||||||
|
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
||||||
import { computed, ComputedRef, defineComponent, onBeforeMount } from 'vue'
|
import { computed, ComputedRef, defineComponent, onBeforeMount } from 'vue'
|
||||||
|
|
||||||
import Loader from '@/components/Common/Loader.vue'
|
import Loader from '@/components/Common/Loader.vue'
|
||||||
@ -23,6 +38,21 @@
|
|||||||
import { IAppConfig } from '@/types/application'
|
import { IAppConfig } from '@/types/application'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
BarElement,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Filler,
|
||||||
|
BarController,
|
||||||
|
CategoryScale,
|
||||||
|
LineController,
|
||||||
|
LinearScale,
|
||||||
|
ChartDataLabels
|
||||||
|
)
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
|
@ -5,25 +5,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { ChartData, ChartOptions, LayoutItem } from 'chart.js'
|
||||||
Chart,
|
|
||||||
ChartData,
|
|
||||||
ChartOptions,
|
|
||||||
LayoutItem,
|
|
||||||
registerables,
|
|
||||||
} from 'chart.js'
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
|
||||||
import { ComputedRef, PropType, computed, defineComponent } from 'vue'
|
import { ComputedRef, PropType, computed, defineComponent } from 'vue'
|
||||||
import { BarChart, useBarChart } from 'vue-chart-3'
|
import { BarChart, useBarChart } from 'vue-chart-3'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { IChartDataset } from '@/types/chart'
|
import { IChartDataset } from '@/types/chart'
|
||||||
import { TDatasetKeys } from '@/types/statistics'
|
import { TStatisticsDatasetKeys } from '@/types/statistics'
|
||||||
import { formatTooltipValue } from '@/utils/tooltip'
|
import { formatTooltipValue } from '@/utils/tooltip'
|
||||||
|
|
||||||
Chart.register(...registerables)
|
|
||||||
Chart.register(ChartDataLabels)
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Chart',
|
name: 'Chart',
|
||||||
components: {
|
components: {
|
||||||
@ -39,7 +29,7 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
displayedData: {
|
displayedData: {
|
||||||
type: String as PropType<TDatasetKeys>,
|
type: String as PropType<TStatisticsDatasetKeys>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import {
|
import {
|
||||||
IStatisticsChartData,
|
IStatisticsChartData,
|
||||||
IStatisticsDateParams,
|
IStatisticsDateParams,
|
||||||
TDatasetKeys,
|
TStatisticsDatasetKeys,
|
||||||
TStatisticsFromApi,
|
TStatisticsFromApi,
|
||||||
} from '@/types/statistics'
|
} from '@/types/statistics'
|
||||||
import { formatStats } from '@/utils/statistics'
|
import { formatStats } from '@/utils/statistics'
|
||||||
@ -32,7 +32,7 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
displayedData: {
|
displayedData: {
|
||||||
type: String as PropType<TDatasetKeys>,
|
type: String as PropType<TStatisticsDatasetKeys>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
|
188
fittrackee_client/src/components/Workout/WorkoutChart/index.vue
Normal file
188
fittrackee_client/src/components/Workout/WorkoutChart/index.vue
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div id="workout-chart">
|
||||||
|
<Card :without-title="false">
|
||||||
|
<template #title>{{ t('workouts.ANALYSIS') }} </template>
|
||||||
|
<template #content>
|
||||||
|
<div class="chart-radio">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="distance"
|
||||||
|
:checked="displayDistance"
|
||||||
|
@click="updateDisplayDistance"
|
||||||
|
/>
|
||||||
|
{{ t('workouts.DISTANCE') }}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="duration"
|
||||||
|
:checked="!displayDistance"
|
||||||
|
@click="updateDisplayDistance"
|
||||||
|
/>
|
||||||
|
{{ t('workouts.DURATION') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<LineChart v-bind="lineChartProps" class="line-chart" />
|
||||||
|
<div class="no-data-cleaning">
|
||||||
|
{{ t('workouts.NO_DATA_CLEANING') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ChartData, ChartOptions } from 'chart.js'
|
||||||
|
import { PropType, defineComponent, ref, ComputedRef, computed } from 'vue'
|
||||||
|
import { LineChart, useLineChart } from 'vue-chart-3'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Card from '@/components/Common/Card.vue'
|
||||||
|
import { IAuthUserProfile } from '@/types/user'
|
||||||
|
import { IWorkoutChartData, IWorkoutState } from '@/types/workouts'
|
||||||
|
import { getDatasets } from '@/utils/workouts'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'WorkoutChart',
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
LineChart,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
authUser: {
|
||||||
|
type: Object as PropType<IAuthUserProfile>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
workout: {
|
||||||
|
type: Object as PropType<IWorkoutState>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { t } = useI18n()
|
||||||
|
let displayDistance = ref(true)
|
||||||
|
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
|
||||||
|
getDatasets(props.workout.chartData, t)
|
||||||
|
)
|
||||||
|
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
|
||||||
|
labels: displayDistance.value
|
||||||
|
? datasets.value.distance_labels
|
||||||
|
: datasets.value.duration_labels,
|
||||||
|
datasets: JSON.parse(
|
||||||
|
JSON.stringify([
|
||||||
|
datasets.value.datasets.speed,
|
||||||
|
datasets.value.datasets.elevation,
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
const options = computed<ChartOptions<'line'>>(() => ({
|
||||||
|
responsive: true,
|
||||||
|
animation: false,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
count: 10,
|
||||||
|
callback: function (value) {
|
||||||
|
return displayDistance.value
|
||||||
|
? Number(value).toFixed(2)
|
||||||
|
: new Date(+value * 1000).toISOString().substr(11, 8)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'linear',
|
||||||
|
bounds: 'data',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: displayDistance.value
|
||||||
|
? t('workouts.DISTANCE') + ' (km)'
|
||||||
|
: t('workouts.DURATION'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ySpeed: {
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: t('workouts.SPEED') + ' (km/h)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yElevation: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: t('workouts.ELEVATION') + ' (m)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
pointStyle: 'circle',
|
||||||
|
pointRadius: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
function updateDisplayDistance() {
|
||||||
|
displayDistance.value = !displayDistance.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lineChartProps } = useLineChart({
|
||||||
|
chartData,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
displayDistance,
|
||||||
|
lineChartProps,
|
||||||
|
t,
|
||||||
|
updateDisplayDistance,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~@/scss/base';
|
||||||
|
#workout-chart {
|
||||||
|
::v-deep(.card) {
|
||||||
|
.card-title {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.chart-radio {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0 $default-padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.no-data-cleaning {
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"ADD_WORKOUT": "Add workout",
|
"ADD_WORKOUT": "Add workout",
|
||||||
|
"ANALYSIS": "analysis",
|
||||||
"ASCENT": "ascent",
|
"ASCENT": "ascent",
|
||||||
"AVERAGE_SPEED": "average speed",
|
"AVERAGE_SPEED": "average speed",
|
||||||
"DESCENT": "descent",
|
"DESCENT": "descent",
|
||||||
"DISTANCE": "distance",
|
"DISTANCE": "distance",
|
||||||
"DURATION": "duration",
|
"DURATION": "duration",
|
||||||
|
"ELEVATION": "elevation",
|
||||||
"END": "end",
|
"END": "end",
|
||||||
"KM": "km",
|
"KM": "km",
|
||||||
"LATEST_WORKOUTS": "Latest workouts",
|
"LATEST_WORKOUTS": "Latest workouts",
|
||||||
@ -12,6 +14,7 @@
|
|||||||
"MAX_SPEED": "max. speed",
|
"MAX_SPEED": "max. speed",
|
||||||
"MIN_ALTITUDE": "min. altitude",
|
"MIN_ALTITUDE": "min. altitude",
|
||||||
"NEXT_WORKOUT": "Next workout",
|
"NEXT_WORKOUT": "Next workout",
|
||||||
|
"NO_DATA_CLEANING": "data from gpx, without any cleaning",
|
||||||
"NO_MAP": "No map",
|
"NO_MAP": "No map",
|
||||||
"NO_NEXT_WORKOUT": "No next workout",
|
"NO_NEXT_WORKOUT": "No next workout",
|
||||||
"NO_PREVIOUS_WORKOUT": "No previous workout",
|
"NO_PREVIOUS_WORKOUT": "No previous workout",
|
||||||
@ -24,6 +27,7 @@
|
|||||||
"RECORD_FD": "Farest distance",
|
"RECORD_FD": "Farest distance",
|
||||||
"RECORD_LD": "Longest duration",
|
"RECORD_LD": "Longest duration",
|
||||||
"RECORD_MS": "Max. speed",
|
"RECORD_MS": "Max. speed",
|
||||||
|
"SPEED": "speed",
|
||||||
"SPORT": "sport | sports",
|
"SPORT": "sport | sports",
|
||||||
"START": "start",
|
"START": "start",
|
||||||
"TOTAL_DURATION": "total duration",
|
"TOTAL_DURATION": "total duration",
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"ADD_WORKOUT": "Ajouter une séance",
|
"ADD_WORKOUT": "Ajouter une séance",
|
||||||
|
"ANALYSIS": "analyse",
|
||||||
"ASCENT": "dénivelé positif",
|
"ASCENT": "dénivelé positif",
|
||||||
"AVERAGE_SPEED": "vitesse moyenne",
|
"AVERAGE_SPEED": "vitesse moyenne",
|
||||||
"DESCENT": "dénivelé négatif",
|
"DESCENT": "dénivelé négatif",
|
||||||
"DISTANCE": "distance",
|
"DISTANCE": "distance",
|
||||||
"DURATION": "durée",
|
"DURATION": "durée",
|
||||||
|
"ELEVATION": "altitude",
|
||||||
"END": "fin",
|
"END": "fin",
|
||||||
"KM": "km",
|
"KM": "km",
|
||||||
"LATEST_WORKOUTS": "Séances récentes",
|
"LATEST_WORKOUTS": "Séances récentes",
|
||||||
@ -12,6 +14,7 @@
|
|||||||
"MAX_SPEED": "vitesse max",
|
"MAX_SPEED": "vitesse max",
|
||||||
"MIN_ALTITUDE": "altitude min",
|
"MIN_ALTITUDE": "altitude min",
|
||||||
"NEXT_WORKOUT": "Séance suivante",
|
"NEXT_WORKOUT": "Séance suivante",
|
||||||
|
"NO_DATA_CLEANING": "données issues du fichier gpx, sans correction",
|
||||||
"NO_MAP": "Pas de carte",
|
"NO_MAP": "Pas de carte",
|
||||||
"NO_NEXT_WORKOUT": "Pas de séance suivante",
|
"NO_NEXT_WORKOUT": "Pas de séance suivante",
|
||||||
"NO_PREVIOUS_WORKOUT": "Pas de séances précédente",
|
"NO_PREVIOUS_WORKOUT": "Pas de séances précédente",
|
||||||
@ -24,6 +27,7 @@
|
|||||||
"RECORD_FD": "Distance la + longue",
|
"RECORD_FD": "Distance la + longue",
|
||||||
"RECORD_LD": "Durée la + longue",
|
"RECORD_LD": "Durée la + longue",
|
||||||
"RECORD_MS": "Vitesse max.",
|
"RECORD_MS": "Vitesse max.",
|
||||||
|
"SPEED": "vitesse",
|
||||||
"SPORT": "sport | sports",
|
"SPORT": "sport | sports",
|
||||||
"START": "début",
|
"START": "début",
|
||||||
"TOTAL_DURATION": "durée totale",
|
"TOTAL_DURATION": "durée totale",
|
||||||
|
@ -64,6 +64,14 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
|
|||||||
res.data.data.workouts[0]
|
res.data.data.workouts[0]
|
||||||
)
|
)
|
||||||
if (res.data.data.workouts[0].with_gpx) {
|
if (res.data.data.workouts[0].with_gpx) {
|
||||||
|
authApi.get(`workouts/${workoutId}/chart_data`).then((res) => {
|
||||||
|
if (res.data.status === 'success') {
|
||||||
|
context.commit(
|
||||||
|
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_CHART_DATA,
|
||||||
|
res.data.data.chart_data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
authApi.get(`workouts/${workoutId}/gpx`).then((res) => {
|
authApi.get(`workouts/${workoutId}/gpx`).then((res) => {
|
||||||
if (res.data.status === 'success') {
|
if (res.data.status === 'success') {
|
||||||
context.commit(
|
context.commit(
|
||||||
|
@ -17,5 +17,6 @@ export enum WorkoutsMutations {
|
|||||||
SET_USER_WORKOUTS = 'SET_USER_WORKOUTS',
|
SET_USER_WORKOUTS = 'SET_USER_WORKOUTS',
|
||||||
SET_WORKOUT = 'SET_WORKOUT',
|
SET_WORKOUT = 'SET_WORKOUT',
|
||||||
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
|
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
|
||||||
|
SET_WORKOUT_CHART_DATA = 'SET_WORKOUT_CHART_DATA',
|
||||||
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
|
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { MutationTree } from 'vuex'
|
import { MutationTree } from 'vuex'
|
||||||
|
|
||||||
import { WORKOUTS_STORE } from '@/store/constants'
|
import { WORKOUTS_STORE } from '@/store/constants'
|
||||||
|
import { initialWorkoutValue } from '@/store/modules/workouts/state'
|
||||||
import {
|
import {
|
||||||
IWorkoutsState,
|
IWorkoutsState,
|
||||||
TWorkoutsMutations,
|
TWorkoutsMutations,
|
||||||
} from '@/store/modules/workouts/types'
|
} from '@/store/modules/workouts/types'
|
||||||
import { IWorkout } from '@/types/workouts'
|
import { IWorkout, IWorkoutApiChartData } from '@/types/workouts'
|
||||||
|
|
||||||
export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_CALENDAR_WORKOUTS](
|
[WORKOUTS_STORE.MUTATIONS.SET_CALENDAR_WORKOUTS](
|
||||||
@ -26,11 +27,11 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
|||||||
) {
|
) {
|
||||||
state.workout.workout = workout
|
state.workout.workout = workout
|
||||||
},
|
},
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_CHART_DATA](
|
||||||
state: IWorkoutsState,
|
state: IWorkoutsState,
|
||||||
loading: boolean
|
chartData: IWorkoutApiChartData[]
|
||||||
) {
|
) {
|
||||||
state.workout.loading = loading
|
state.workout.chartData = chartData
|
||||||
},
|
},
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](
|
||||||
state: IWorkoutsState,
|
state: IWorkoutsState,
|
||||||
@ -38,15 +39,17 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
|||||||
) {
|
) {
|
||||||
state.workout.gpx = gpx
|
state.workout.gpx = gpx
|
||||||
},
|
},
|
||||||
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
||||||
|
state: IWorkoutsState,
|
||||||
|
loading: boolean
|
||||||
|
) {
|
||||||
|
state.workout.loading = loading
|
||||||
|
},
|
||||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: IWorkoutsState) {
|
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: IWorkoutsState) {
|
||||||
state.calendar_workouts = []
|
state.calendar_workouts = []
|
||||||
state.user_workouts = []
|
state.user_workouts = []
|
||||||
},
|
},
|
||||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: IWorkoutsState) {
|
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: IWorkoutsState) {
|
||||||
state.workout = {
|
state.workout = initialWorkoutValue
|
||||||
gpx: '',
|
|
||||||
loading: false,
|
|
||||||
workout: <IWorkout>{},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { IWorkoutsState } from '@/store/modules/workouts/types'
|
import { IWorkoutsState } from '@/store/modules/workouts/types'
|
||||||
import { IWorkout } from '@/types/workouts'
|
import { IWorkout } from '@/types/workouts'
|
||||||
|
|
||||||
|
export const initialWorkoutValue = {
|
||||||
|
gpx: '',
|
||||||
|
loading: false,
|
||||||
|
workout: <IWorkout>{},
|
||||||
|
chartData: [],
|
||||||
|
}
|
||||||
|
|
||||||
export const workoutsState: IWorkoutsState = {
|
export const workoutsState: IWorkoutsState = {
|
||||||
calendar_workouts: [],
|
calendar_workouts: [],
|
||||||
user_workouts: [],
|
user_workouts: [],
|
||||||
workout: {
|
workout: initialWorkoutValue,
|
||||||
gpx: '',
|
|
||||||
loading: false,
|
|
||||||
workout: <IWorkout>{},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,12 @@ import {
|
|||||||
|
|
||||||
import { WORKOUTS_STORE } from '@/store/constants'
|
import { WORKOUTS_STORE } from '@/store/constants'
|
||||||
import { IRootState } from '@/store/modules/root/types'
|
import { IRootState } from '@/store/modules/root/types'
|
||||||
import { IWorkout, IWorkoutsPayload, IWorkoutState } from '@/types/workouts'
|
import {
|
||||||
|
IWorkout,
|
||||||
|
IWorkoutApiChartData,
|
||||||
|
IWorkoutsPayload,
|
||||||
|
IWorkoutState,
|
||||||
|
} from '@/types/workouts'
|
||||||
|
|
||||||
export interface IWorkoutsState {
|
export interface IWorkoutsState {
|
||||||
user_workouts: IWorkout[]
|
user_workouts: IWorkout[]
|
||||||
@ -46,6 +51,10 @@ export type TWorkoutsMutations<S = IWorkoutsState> = {
|
|||||||
workouts: IWorkout[]
|
workouts: IWorkout[]
|
||||||
): void
|
): void
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](state: S, workout: IWorkout): void
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](state: S, workout: IWorkout): void
|
||||||
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_CHART_DATA](
|
||||||
|
state: S,
|
||||||
|
chartDate: IWorkoutApiChartData[]
|
||||||
|
): void
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](state: S, gpx: string): void
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](state: S, gpx: string): void
|
||||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
||||||
state: S,
|
state: S,
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
export interface IChartDataset {
|
export interface IChartDataset {
|
||||||
label: string
|
label: string
|
||||||
backgroundColor: string[]
|
backgroundColor: string[]
|
||||||
|
borderColor?: string[]
|
||||||
|
borderWidth?: number
|
||||||
|
fill?: boolean
|
||||||
data: number[]
|
data: number[]
|
||||||
|
yAxisID?: string
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,13 @@ export interface IStatisticsDateParams {
|
|||||||
end: Date
|
end: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TDatasetKeys = 'nb_workouts' | 'total_duration' | 'total_distance'
|
export type TStatisticsDatasetKeys =
|
||||||
|
| 'nb_workouts'
|
||||||
|
| 'total_duration'
|
||||||
|
| 'total_distance'
|
||||||
|
|
||||||
export type TStatistics = {
|
export type TStatistics = {
|
||||||
[key in TDatasetKeys]: number
|
[key in TStatisticsDatasetKeys]: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSportStatistics = {
|
export type TSportStatistics = {
|
||||||
@ -33,7 +36,7 @@ export type TStatisticsFromApi = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TStatisticsDatasets = {
|
export type TStatisticsDatasets = {
|
||||||
[key in TDatasetKeys]: IChartDataset[]
|
[key in TStatisticsDatasetKeys]: IChartDataset[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStatisticsChartData {
|
export interface IStatisticsChartData {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IChartDataset } from '@/types/chart'
|
||||||
|
|
||||||
export interface IWorkoutSegment {
|
export interface IWorkoutSegment {
|
||||||
ascent: number
|
ascent: number
|
||||||
ave_speed: number
|
ave_speed: number
|
||||||
@ -79,8 +81,31 @@ export interface IWorkoutsPayload {
|
|||||||
page?: number
|
page?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkoutApiChartData {
|
||||||
|
distance: number
|
||||||
|
duration: number
|
||||||
|
elevation: number
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
speed: number
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkoutState {
|
export interface IWorkoutState {
|
||||||
gpx: string
|
gpx: string
|
||||||
loading: boolean
|
loading: boolean
|
||||||
workout: IWorkout
|
workout: IWorkout
|
||||||
|
chartData: IWorkoutApiChartData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TWorkoutDatasetKeys = 'speed' | 'elevation'
|
||||||
|
|
||||||
|
export type TWorkoutDatasets = {
|
||||||
|
[key in TWorkoutDatasetKeys]: IChartDataset
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkoutChartData {
|
||||||
|
distance_labels: unknown[]
|
||||||
|
duration_labels: unknown[]
|
||||||
|
datasets: TWorkoutDatasets
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { ISport } from '@/types/sports'
|
|||||||
import {
|
import {
|
||||||
IStatisticsChartData,
|
IStatisticsChartData,
|
||||||
IStatisticsDateParams,
|
IStatisticsDateParams,
|
||||||
TDatasetKeys,
|
TStatisticsDatasetKeys,
|
||||||
TStatisticsDatasets,
|
TStatisticsDatasets,
|
||||||
TStatisticsFromApi,
|
TStatisticsFromApi,
|
||||||
} from '@/types/statistics'
|
} from '@/types/statistics'
|
||||||
@ -19,7 +19,7 @@ const dateFormats: Record<string, string> = {
|
|||||||
year: 'yyyy',
|
year: 'yyyy',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const datasetKeys: TDatasetKeys[] = [
|
export const datasetKeys: TStatisticsDatasetKeys[] = [
|
||||||
'nb_workouts',
|
'nb_workouts',
|
||||||
'total_duration',
|
'total_duration',
|
||||||
'total_distance',
|
'total_distance',
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { TDatasetKeys } from '@/types/statistics'
|
import { TStatisticsDatasetKeys } from '@/types/statistics'
|
||||||
import { formatDuration } from '@/utils/duration'
|
import { formatDuration } from '@/utils/duration'
|
||||||
|
|
||||||
export const formatTooltipValue = (
|
export const formatTooltipValue = (
|
||||||
displayedData: TDatasetKeys,
|
displayedData: TStatisticsDatasetKeys,
|
||||||
value: number,
|
value: number,
|
||||||
formatWithUnits = true
|
formatWithUnits = true
|
||||||
): string => {
|
): string => {
|
||||||
|
41
fittrackee_client/src/utils/workouts.ts
Normal file
41
fittrackee_client/src/utils/workouts.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
IWorkoutApiChartData,
|
||||||
|
IWorkoutChartData,
|
||||||
|
TWorkoutDatasets,
|
||||||
|
} from '@/types/workouts'
|
||||||
|
|
||||||
|
export const getDatasets = (
|
||||||
|
chartData: IWorkoutApiChartData[],
|
||||||
|
t: CallableFunction
|
||||||
|
): IWorkoutChartData => {
|
||||||
|
const datasets: TWorkoutDatasets = {
|
||||||
|
speed: {
|
||||||
|
label: t('workouts.SPEED'),
|
||||||
|
backgroundColor: ['#FFFFFF'],
|
||||||
|
borderColor: ['#8884d8'],
|
||||||
|
borderWidth: 2,
|
||||||
|
data: [],
|
||||||
|
yAxisID: 'ySpeed',
|
||||||
|
},
|
||||||
|
elevation: {
|
||||||
|
label: t('workouts.ELEVATION'),
|
||||||
|
backgroundColor: ['#e5e5e5'],
|
||||||
|
borderColor: ['#cccccc'],
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: true,
|
||||||
|
data: [],
|
||||||
|
yAxisID: 'yElevation',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const distance_labels: unknown[] = []
|
||||||
|
const duration_labels: unknown[] = []
|
||||||
|
|
||||||
|
chartData.map((data) => {
|
||||||
|
distance_labels.push(data.distance)
|
||||||
|
duration_labels.push(data.duration)
|
||||||
|
datasets.speed.data.push(data.speed)
|
||||||
|
datasets.elevation.data.push(data.elevation)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { distance_labels, duration_labels, datasets }
|
||||||
|
}
|
@ -14,6 +14,11 @@
|
|||||||
:sports="sports"
|
:sports="sports"
|
||||||
:authUser="authUser"
|
:authUser="authUser"
|
||||||
/>
|
/>
|
||||||
|
<WorkoutChart
|
||||||
|
v-if="workout.chartData.length > 0"
|
||||||
|
:workout="workout"
|
||||||
|
:authUser="authUser"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<NotFound target="WORKOUT" />
|
<NotFound target="WORKOUT" />
|
||||||
@ -35,6 +40,7 @@
|
|||||||
|
|
||||||
import Loader from '@/components/Common/Loader.vue'
|
import Loader from '@/components/Common/Loader.vue'
|
||||||
import NotFound from '@/components/Common/NotFound.vue'
|
import NotFound from '@/components/Common/NotFound.vue'
|
||||||
|
import WorkoutChart from '@/components/Workout/WorkoutChart/index.vue'
|
||||||
import WorkoutDetail from '@/components/Workout/WorkoutDetail/index.vue'
|
import WorkoutDetail from '@/components/Workout/WorkoutDetail/index.vue'
|
||||||
import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
|
import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
|
||||||
import { IAuthUserProfile } from '@/types/user'
|
import { IAuthUserProfile } from '@/types/user'
|
||||||
@ -46,6 +52,7 @@
|
|||||||
components: {
|
components: {
|
||||||
Loader,
|
Loader,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
WorkoutChart,
|
||||||
WorkoutDetail,
|
WorkoutDetail,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
@ -78,6 +85,7 @@
|
|||||||
@import '~@/scss/base';
|
@import '~@/scss/base';
|
||||||
#workout {
|
#workout {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-bottom: 45px;
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
109
fittrackee_client/tests/unit/utils/workouts.spec.ts
Normal file
109
fittrackee_client/tests/unit/utils/workouts.spec.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { assert } from 'chai'
|
||||||
|
|
||||||
|
import createI18n from '@/i18n'
|
||||||
|
import { getDatasets } from '@/utils/workouts'
|
||||||
|
|
||||||
|
const { t, locale } = createI18n.global
|
||||||
|
|
||||||
|
describe('getDatasets', () => {
|
||||||
|
const testparams = [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'returns empty datasets when no chart data w/ french translation',
|
||||||
|
inputParams: {
|
||||||
|
charData: [],
|
||||||
|
locale: 'fr',
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
distance_labels: [],
|
||||||
|
duration_labels: [],
|
||||||
|
datasets: {
|
||||||
|
speed: {
|
||||||
|
label: 'vitesse',
|
||||||
|
backgroundColor: ['#FFFFFF'],
|
||||||
|
borderColor: ['#8884d8'],
|
||||||
|
borderWidth: 2,
|
||||||
|
data: [],
|
||||||
|
yAxisID: 'ySpeed',
|
||||||
|
},
|
||||||
|
elevation: {
|
||||||
|
label: 'altitude',
|
||||||
|
backgroundColor: ['#e5e5e5'],
|
||||||
|
borderColor: ['#cccccc'],
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: true,
|
||||||
|
data: [],
|
||||||
|
yAxisID: 'yElevation',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns datasets w/ english translation',
|
||||||
|
inputParams: {
|
||||||
|
charData: [
|
||||||
|
{
|
||||||
|
distance: 0,
|
||||||
|
duration: 0,
|
||||||
|
elevation: 83.6,
|
||||||
|
latitude: 48.845574,
|
||||||
|
longitude: 2.373723,
|
||||||
|
speed: 2.89,
|
||||||
|
time: 'Sun, 12 Sep 2021 13:29:24 GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 0,
|
||||||
|
duration: 1,
|
||||||
|
elevation: 83.7,
|
||||||
|
latitude: 48.845578,
|
||||||
|
longitude: 2.373732,
|
||||||
|
speed: 1.56,
|
||||||
|
time: 'Sun, 12 Sep 2021 13:29:25 GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 0.01,
|
||||||
|
duration: 96,
|
||||||
|
elevation: 84.3,
|
||||||
|
latitude: 48.845591,
|
||||||
|
longitude: 2.373811,
|
||||||
|
speed: 14.73,
|
||||||
|
time: 'Sun, 12 Sep 2021 13:31:00 GMT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
distance_labels: [0, 0, 0.01],
|
||||||
|
duration_labels: [0, 1, 96],
|
||||||
|
datasets: {
|
||||||
|
speed: {
|
||||||
|
label: 'speed',
|
||||||
|
backgroundColor: ['#FFFFFF'],
|
||||||
|
borderColor: ['#8884d8'],
|
||||||
|
borderWidth: 2,
|
||||||
|
data: [2.89, 1.56, 14.73],
|
||||||
|
yAxisID: 'ySpeed',
|
||||||
|
},
|
||||||
|
elevation: {
|
||||||
|
label: 'elevation',
|
||||||
|
backgroundColor: ['#e5e5e5'],
|
||||||
|
borderColor: ['#cccccc'],
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: true,
|
||||||
|
data: [83.6, 83.7, 84.3],
|
||||||
|
yAxisID: 'yElevation',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
testparams.map((testParams) => {
|
||||||
|
it(testParams.description, () => {
|
||||||
|
locale.value = testParams.inputParams.locale
|
||||||
|
assert.deepEqual(
|
||||||
|
getDatasets(testParams.inputParams.charData, t),
|
||||||
|
testParams.expected
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user