Client - improve calendar display on desktop and mobile (wip)
This commit is contained in:
parent
d9790e0465
commit
ba0b94de45
@ -138,7 +138,7 @@
|
|||||||
|
|
||||||
.calendar-cell {
|
.calendar-cell {
|
||||||
border-right: solid 1px var(--calendar-border-color);
|
border-right: solid 1px var(--calendar-border-color);
|
||||||
height: 3em;
|
height: 40px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-basis: 8%;
|
flex-basis: 8%;
|
||||||
padding: $default-padding * 0.5 $default-padding $default-padding * 0.5
|
padding: $default-padding * 0.5 $default-padding $default-padding * 0.5
|
||||||
|
@ -50,7 +50,8 @@
|
|||||||
@import '~@/scss/base';
|
@import '~@/scss/base';
|
||||||
|
|
||||||
.calendar-workout {
|
.calendar-workout {
|
||||||
padding: 2px 0;
|
display: flex;
|
||||||
|
padding: 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
img {
|
img {
|
||||||
max-width: 18px;
|
max-width: 18px;
|
||||||
@ -58,8 +59,9 @@
|
|||||||
}
|
}
|
||||||
sup {
|
sup {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.6em;
|
top: -8px;
|
||||||
left: -0.4em;
|
left: -3px;
|
||||||
|
width: 2px;
|
||||||
.custom-fa-small {
|
.custom-fa-small {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="calendar-workouts">
|
<div class="calendar-workouts">
|
||||||
<div v-for="(workout, index) in workouts.slice(0, 6)" :key="index">
|
<div class="desktop-display">
|
||||||
<CalendarWorkout
|
|
||||||
:workout="workout"
|
|
||||||
:sportImg="getSportImg(workout, sports)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="workouts.length > 6">
|
|
||||||
<i
|
|
||||||
class="fa calendar-more"
|
|
||||||
:class="`fa-${isHidden ? 'plus' : 'times'}`"
|
|
||||||
aria-hidden="true"
|
|
||||||
title="show more workouts"
|
|
||||||
@click="displayMore"
|
|
||||||
/>
|
|
||||||
<div v-if="!isHidden" class="more-workouts">
|
|
||||||
<div
|
<div
|
||||||
v-for="(workout, index) in workouts.slice(6 - workouts.length)"
|
class="workouts-display"
|
||||||
:key="index"
|
v-if="workouts.length <= displayedWorkoutCount"
|
||||||
>
|
>
|
||||||
<CalendarWorkout
|
<CalendarWorkout
|
||||||
|
v-for="(workout, index) in workouts.slice(0, displayedWorkoutCount)"
|
||||||
|
:key="index"
|
||||||
:workout="workout"
|
:workout="workout"
|
||||||
:sportImg="getSportImg(workout, sports)"
|
:sportImg="getSportImg(workout, sports)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="donut-display">
|
||||||
|
<CalendarWorkoutsChart
|
||||||
|
:workouts="workouts"
|
||||||
|
:sports="sports"
|
||||||
|
:datasets="chartDatasets"
|
||||||
|
:colors="colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-display">
|
||||||
|
<div class="donut-display" v-if="workouts.length > 0">
|
||||||
|
<CalendarWorkoutsChart
|
||||||
|
:workouts="workouts"
|
||||||
|
:sports="sports"
|
||||||
|
:datasets="chartDatasets"
|
||||||
|
:colors="colors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PropType, defineComponent, ref } from 'vue'
|
import { PropType, computed, defineComponent } from 'vue'
|
||||||
|
|
||||||
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
|
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
|
||||||
|
import CalendarWorkoutsChart from '@/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue'
|
||||||
import { ISport } from '@/types/sports'
|
import { ISport } from '@/types/sports'
|
||||||
import { IWorkout } from '@/types/workouts'
|
import { IWorkout } from '@/types/workouts'
|
||||||
|
import { getSportImg, sportIdColors } from '@/utils/sports'
|
||||||
|
import { getDonutDatasets } from '@/utils/workouts'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CalendarWorkouts',
|
name: 'CalendarWorkouts',
|
||||||
components: {
|
components: {
|
||||||
CalendarWorkout,
|
CalendarWorkout,
|
||||||
|
CalendarWorkoutsChart,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
workouts: {
|
workouts: {
|
||||||
@ -51,17 +60,13 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup(props) {
|
||||||
const isHidden = ref(true)
|
return {
|
||||||
function getSportImg(workout: IWorkout, sports: ISport[]): string {
|
chartDatasets: computed(() => getDonutDatasets(props.workouts)),
|
||||||
return sports
|
colors: computed(() => sportIdColors(props.sports)),
|
||||||
.filter((sport) => sport.id === workout.sport_id)
|
displayedWorkoutCount: 6,
|
||||||
.map((sport) => sport.img)[0]
|
getSportImg,
|
||||||
}
|
}
|
||||||
function displayMore() {
|
|
||||||
isHidden.value = !isHidden.value
|
|
||||||
}
|
|
||||||
return { isHidden, getSportImg, displayMore }
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -69,32 +74,32 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~@/scss/base';
|
@import '~@/scss/base';
|
||||||
.calendar-workouts {
|
.calendar-workouts {
|
||||||
|
.desktop-display {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.mobile-display {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workouts-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0 $default-padding 0 0;
|
||||||
.calendar-more {
|
|
||||||
position: absolute;
|
|
||||||
top: 30px;
|
|
||||||
right: -3px;
|
|
||||||
}
|
}
|
||||||
.more-workouts {
|
.donut-display {
|
||||||
background: whitesmoke;
|
|
||||||
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
|
|
||||||
0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
|
||||||
position: absolute;
|
|
||||||
top: 52px;
|
|
||||||
left: 0;
|
|
||||||
min-width: 53px;
|
|
||||||
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
height: 34px;
|
||||||
z-index: 1000;
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-limit) {
|
||||||
|
.desktop-display {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mobile-display {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar-workouts-chart">
|
||||||
|
<div class="workouts-chart" @click="togglePane">
|
||||||
|
<div class="workouts-count">{{ workouts.length }}</div>
|
||||||
|
<DonutChart :datasets="datasets" :colors="colors" />
|
||||||
|
</div>
|
||||||
|
<div class="workouts-pane" v-if="!isHidden">
|
||||||
|
<div class="more-workouts" v-click-outside="togglePane">
|
||||||
|
<i
|
||||||
|
class="fa fa-times calendar-more"
|
||||||
|
aria-hidden="true"
|
||||||
|
@click="togglePane"
|
||||||
|
/>
|
||||||
|
<div v-for="(workout, index) in workouts" :key="index">
|
||||||
|
<CalendarWorkout
|
||||||
|
:workout="workout"
|
||||||
|
:sportImg="getSportImg(workout, sports)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent, ref } from 'vue'
|
||||||
|
|
||||||
|
import CalendarWorkout from '@/components/Dashboard/UserCalendar/CalendarWorkout.vue'
|
||||||
|
import DonutChart from '@/components/Dashboard/UserCalendar/DonutChart.vue'
|
||||||
|
import { ISport } from '@/types/sports'
|
||||||
|
import { IWorkout } from '@/types/workouts'
|
||||||
|
import { getSportImg } from '@/utils/sports'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CalendarWorkoutsChart',
|
||||||
|
components: {
|
||||||
|
CalendarWorkout,
|
||||||
|
DonutChart,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
colors: {
|
||||||
|
type: Object as PropType<Record<number, string>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
datasets: {
|
||||||
|
type: Object as PropType<Record<number, Record<string, number>>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sports: {
|
||||||
|
type: Object as PropType<ISport[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
workouts: {
|
||||||
|
type: Object as PropType<IWorkout[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const isHidden = ref(true)
|
||||||
|
function togglePane(event: Event & { target: HTMLElement }) {
|
||||||
|
event.stopPropagation()
|
||||||
|
isHidden.value = !isHidden.value
|
||||||
|
}
|
||||||
|
return { isHidden, getSportImg, togglePane }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~@/scss/base';
|
||||||
|
.calendar-workouts-chart {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.workouts-chart {
|
||||||
|
position: relative;
|
||||||
|
.workouts-count {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 6px;
|
||||||
|
width: 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: $small-limit) {
|
||||||
|
.workouts-count {
|
||||||
|
top: 16px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
::v-deep(.donut-chart) {
|
||||||
|
padding-top: 12px;
|
||||||
|
svg g circle {
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workouts-pane {
|
||||||
|
display: flex;
|
||||||
|
padding-left: 40px;
|
||||||
|
|
||||||
|
.more-workouts {
|
||||||
|
background: whitesmoke;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
|
||||||
|
0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
|
position: absolute;
|
||||||
|
top: 52px;
|
||||||
|
left: 0;
|
||||||
|
min-width: 60px;
|
||||||
|
@media screen and (max-width: $small-limit) {
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
.calendar-more {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 0.9em;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,73 @@
|
|||||||
|
// adapted from: https://css-tricks.com/building-a-donut-chart-with-vue-and-svg/
|
||||||
|
<template>
|
||||||
|
<div class="donut-chart">
|
||||||
|
<svg height="34" width="34" viewBox="0 0 34 34">
|
||||||
|
<g v-for="(data, index) of Object.entries(datasets)" :key="index">
|
||||||
|
<circle
|
||||||
|
:cx="cx"
|
||||||
|
:cy="cy"
|
||||||
|
:r="radius"
|
||||||
|
fill="transparent"
|
||||||
|
:stroke="colors[+data[0]]"
|
||||||
|
:stroke-dashoffset="
|
||||||
|
calculateStrokeDashOffset(data[1].percentage, circumference)
|
||||||
|
"
|
||||||
|
:stroke-dasharray="circumference"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-opacity="0.8"
|
||||||
|
:transform="returnCircleTransformValue(index, data[1].percentage)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DonutChart',
|
||||||
|
props: {
|
||||||
|
colors: {
|
||||||
|
type: Object as PropType<Record<number, string>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
datasets: {
|
||||||
|
type: Object as PropType<Record<number, Record<string, number>>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
let angleOffset = -90
|
||||||
|
const cx = 16
|
||||||
|
const cy = 16
|
||||||
|
const radius = 14
|
||||||
|
const circumference = 2 * Math.PI * radius
|
||||||
|
|
||||||
|
function calculateStrokeDashOffset(
|
||||||
|
percentage: number,
|
||||||
|
circumference: number
|
||||||
|
): number {
|
||||||
|
return circumference - percentage * circumference
|
||||||
|
}
|
||||||
|
function returnCircleTransformValue(
|
||||||
|
index: number,
|
||||||
|
percentage: number
|
||||||
|
): string {
|
||||||
|
const rotation = `rotate(${angleOffset}, ${cx}, ${cy})`
|
||||||
|
angleOffset = percentage * 360 + angleOffset
|
||||||
|
return rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
angleOffset,
|
||||||
|
circumference,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
radius,
|
||||||
|
calculateStrokeDashOffset,
|
||||||
|
returnCircleTransformValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
27
fittrackee_client/src/directives.ts
Normal file
27
fittrackee_client/src/directives.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Directive, DirectiveBinding } from 'vue'
|
||||||
|
|
||||||
|
interface ClickOutsideHTMLElement extends HTMLElement {
|
||||||
|
clickOutsideEvent?: (event: MouseEvent | TouchEvent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clickOutsideDirective: Directive = {
|
||||||
|
mounted: (
|
||||||
|
element: ClickOutsideHTMLElement,
|
||||||
|
binding: DirectiveBinding
|
||||||
|
): void => {
|
||||||
|
element.clickOutsideEvent = function (event) {
|
||||||
|
if (!(element === event.target || element.contains(<Node>event.target))) {
|
||||||
|
binding.value(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.addEventListener('click', element.clickOutsideEvent)
|
||||||
|
document.body.addEventListener('touchstart', element.clickOutsideEvent)
|
||||||
|
},
|
||||||
|
unmounted: function (element: ClickOutsideHTMLElement): void {
|
||||||
|
if (element.clickOutsideEvent) {
|
||||||
|
document.body.removeEventListener('click', element.clickOutsideEvent)
|
||||||
|
document.body.removeEventListener('touchstart', element.clickOutsideEvent)
|
||||||
|
element.clickOutsideEvent = undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@ -6,4 +6,11 @@ import i18n from './i18n'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
createApp(App).use(i18n).use(store).use(router).mount('#app')
|
import { clickOutsideDirective } from '@/directives'
|
||||||
|
|
||||||
|
createApp(App)
|
||||||
|
.use(i18n)
|
||||||
|
.use(store)
|
||||||
|
.use(router)
|
||||||
|
.directive('click-outside', clickOutsideDirective)
|
||||||
|
.mount('#app')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ISport } from '@/types/sports'
|
import { ISport } from '@/types/sports'
|
||||||
|
import { IWorkout } from '@/types/workouts'
|
||||||
|
|
||||||
export const sportColors: Record<string, string> = {
|
export const sportColors: Record<string, string> = {
|
||||||
'Cycling (Sport)': '#55A8A3',
|
'Cycling (Sport)': '#55A8A3',
|
||||||
@ -9,6 +10,12 @@ export const sportColors: Record<string, string> = {
|
|||||||
Walking: '#929292',
|
Walking: '#929292',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sportIdColors = (sports: ISport[]): Record<number, string> => {
|
||||||
|
const colors: Record<number, string> = {}
|
||||||
|
sports.map((sport) => (colors[sport.id] = sportColors[sport.label]))
|
||||||
|
return colors
|
||||||
|
}
|
||||||
|
|
||||||
const sortSports = (a: ISport, b: ISport): number => {
|
const sortSports = (a: ISport, b: ISport): number => {
|
||||||
const sportALabel = a.label.toLowerCase()
|
const sportALabel = a.label.toLowerCase()
|
||||||
const sportBLabel = b.label.toLowerCase()
|
const sportBLabel = b.label.toLowerCase()
|
||||||
@ -27,3 +34,9 @@ export const translateSports = (
|
|||||||
label: t(`sports.${sport.label}.LABEL`),
|
label: t(`sports.${sport.label}.LABEL`),
|
||||||
}))
|
}))
|
||||||
.sort(sortSports)
|
.sort(sortSports)
|
||||||
|
|
||||||
|
export const getSportImg = (workout: IWorkout, sports: ISport[]): string => {
|
||||||
|
return sports
|
||||||
|
.filter((sport) => sport.id === workout.sport_id)
|
||||||
|
.map((sport) => sport.img)[0]
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
IWorkout,
|
||||||
IWorkoutApiChartData,
|
IWorkoutApiChartData,
|
||||||
IWorkoutChartData,
|
IWorkoutChartData,
|
||||||
TCoordinates,
|
TCoordinates,
|
||||||
@ -42,3 +43,27 @@ export const getDatasets = (
|
|||||||
|
|
||||||
return { distance_labels, duration_labels, datasets, coordinates }
|
return { distance_labels, duration_labels, datasets, coordinates }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDonutDatasets = (
|
||||||
|
workouts: IWorkout[]
|
||||||
|
): Record<number, Record<string, number>> => {
|
||||||
|
const total = workouts.length
|
||||||
|
if (total === 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasets: Record<number, Record<string, number>> = {}
|
||||||
|
workouts.map((workout) => {
|
||||||
|
if (!datasets[workout.sport_id]) {
|
||||||
|
datasets[workout.sport_id] = {
|
||||||
|
count: 0,
|
||||||
|
percentage: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
datasets[workout.sport_id].count += 1
|
||||||
|
datasets[workout.sport_id].percentage =
|
||||||
|
datasets[workout.sport_id].count / total
|
||||||
|
})
|
||||||
|
|
||||||
|
return datasets
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { assert } from 'chai'
|
import { assert } from 'chai'
|
||||||
|
|
||||||
import createI18n from '@/i18n'
|
import createI18n from '@/i18n'
|
||||||
import { getDatasets } from '@/utils/workouts'
|
import { getDatasets, getDonutDatasets } from '@/utils/workouts'
|
||||||
|
|
||||||
const { t, locale } = createI18n.global
|
const { t, locale } = createI18n.global
|
||||||
|
|
||||||
@ -113,3 +113,155 @@ describe('getDatasets', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getDonutDatasets', () => {
|
||||||
|
const testparams = [
|
||||||
|
{
|
||||||
|
description: 'returns empty datasets when no workouts provided',
|
||||||
|
input: [],
|
||||||
|
expected: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns donut chart datasets w/ count and percentage',
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
ascent: null,
|
||||||
|
ave_speed: 10.0,
|
||||||
|
bounds: [],
|
||||||
|
creation_date: 'Sun, 14 Jul 2019 13:51:01 GMT',
|
||||||
|
descent: null,
|
||||||
|
distance: 10.0,
|
||||||
|
duration: '0:17:04',
|
||||||
|
id: 'TfJ9nHVvoyxF2B8YBmMDB8',
|
||||||
|
map: null,
|
||||||
|
max_alt: null,
|
||||||
|
max_speed: 10.0,
|
||||||
|
min_alt: null,
|
||||||
|
modification_date: null,
|
||||||
|
moving: '0:17:04',
|
||||||
|
next_workout: 'kjxavSTUrJvoAh2wvCeGEF',
|
||||||
|
notes: '',
|
||||||
|
pauses: null,
|
||||||
|
previous_workout: null,
|
||||||
|
records: [],
|
||||||
|
segments: [],
|
||||||
|
sport_id: 2,
|
||||||
|
title: 'Cycling (Transport)',
|
||||||
|
user: 'admin',
|
||||||
|
weather_end: null,
|
||||||
|
weather_start: null,
|
||||||
|
with_gpx: false,
|
||||||
|
workout_date: 'Mon, 01 Jan 2018 00:00:00 GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ascent: null,
|
||||||
|
ave_speed: 16,
|
||||||
|
bounds: [],
|
||||||
|
creation_date: 'Sun, 14 Jul 2019 18:57:14 GMT',
|
||||||
|
descent: null,
|
||||||
|
distance: 12,
|
||||||
|
duration: '0:45:00',
|
||||||
|
id: 'kjxavSTUrJvoAh2wvCeGEF',
|
||||||
|
map: null,
|
||||||
|
max_alt: null,
|
||||||
|
max_speed: 16,
|
||||||
|
min_alt: null,
|
||||||
|
modification_date: 'Sun, 14 Jul 2019 18:57:22 GMT',
|
||||||
|
moving: '0:45:00',
|
||||||
|
next_workout: 'TfJ9nHVvoyxF2B8YBmMDB8',
|
||||||
|
notes: 'workout without gpx',
|
||||||
|
pauses: null,
|
||||||
|
previous_workout: 'TfJ9nHVvoyxF2B8YBmMDB8',
|
||||||
|
records: [],
|
||||||
|
segments: [],
|
||||||
|
sport_id: 1,
|
||||||
|
title: 'biking on sunday morning',
|
||||||
|
user: 'admin',
|
||||||
|
weather_end: null,
|
||||||
|
weather_start: null,
|
||||||
|
with_gpx: false,
|
||||||
|
workout_date: 'Sun, 07 Jul 2019 07:00:00 GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ascent: null,
|
||||||
|
ave_speed: 5.31,
|
||||||
|
bounds: [],
|
||||||
|
creation_date: 'Wed, 29 Sep 2021 06:18:44 GMT',
|
||||||
|
descent: null,
|
||||||
|
distance: 6.3,
|
||||||
|
duration: '1:11:10',
|
||||||
|
id: 'eYwTr2A5L6xX52rwwrfL4A',
|
||||||
|
map: null,
|
||||||
|
max_alt: null,
|
||||||
|
max_speed: 5.31,
|
||||||
|
min_alt: null,
|
||||||
|
modification_date: 'Wed, 29 Sep 2021 06:54:02 GMT',
|
||||||
|
moving: '1:11:10',
|
||||||
|
next_workout: 'oN4kVTRCdsy2cGNKANSJKM',
|
||||||
|
notes: '',
|
||||||
|
pauses: null,
|
||||||
|
previous_workout: 'kjxavSTUrJvoAh2wvCeGEF',
|
||||||
|
records: [],
|
||||||
|
segments: [],
|
||||||
|
sport_id: 2,
|
||||||
|
title: 'Cycling (Transport) - 2021-09-21 21:00:00',
|
||||||
|
user: 'admin',
|
||||||
|
weather_end: null,
|
||||||
|
weather_start: null,
|
||||||
|
with_gpx: false,
|
||||||
|
workout_date: 'Tue, 21 Sep 2021 19:00:00 GMT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ascent: null,
|
||||||
|
ave_speed: 3.97,
|
||||||
|
bounds: [],
|
||||||
|
creation_date: 'Thu, 30 Sep 2021 18:55:54 GMT',
|
||||||
|
descent: null,
|
||||||
|
distance: 5,
|
||||||
|
duration: '1:15:30',
|
||||||
|
id: 'FiRvMtGJCp56dqN8qfn8BK',
|
||||||
|
map: null,
|
||||||
|
max_alt: null,
|
||||||
|
max_speed: 3.97,
|
||||||
|
min_alt: null,
|
||||||
|
modification_date: null,
|
||||||
|
moving: '1:15:30',
|
||||||
|
next_workout: '2GZm7YgULHi9b4kCHDbHsY',
|
||||||
|
notes: '',
|
||||||
|
pauses: null,
|
||||||
|
previous_workout: '2GZm7YgULHi9b4kCHDbHsY',
|
||||||
|
records: [],
|
||||||
|
segments: [],
|
||||||
|
sport_id: 3,
|
||||||
|
title: 'just hiking',
|
||||||
|
user: 'admin',
|
||||||
|
weather_end: null,
|
||||||
|
weather_start: null,
|
||||||
|
with_gpx: false,
|
||||||
|
workout_date: 'Mon, 20 Sep 2021 07:00:00 GMT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
1: {
|
||||||
|
count: 1,
|
||||||
|
percentage: 0.25,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
count: 2,
|
||||||
|
percentage: 0.5,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
count: 1,
|
||||||
|
percentage: 0.25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
testparams.map((testParams) => {
|
||||||
|
it(testParams.description, () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
assert.deepEqual(getDonutDatasets(testParams.input), testParams.expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user