mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 07:47:58 -05:00
Add HR Zones chart to mobile, move logic to chartUtils
[frontend] moved chart functions to chartUtils file [frontend] added HR Zones Bar Chart to mobile view
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div if="activity" class="fw-lighter">
|
||||
<!-- laps -->
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units" v-if="activityActivityLaps && activityActivityLaps.length > 0"/>
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units"
|
||||
v-if="activityActivityLaps && activityActivityLaps.length > 0" />
|
||||
|
||||
<!-- Pace values -->
|
||||
<div v-if="pacePresent">
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitlePace") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'pace'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'pace'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="formattedPace">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgPace") }}
|
||||
@@ -40,14 +42,19 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleSpeed") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'vel'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'vel'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_speed">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgSpeed") }}
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="activity.average_speed && Number(units) === 1"><b>{{ formatAverageSpeedMetric(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsKmH") }}</b></span>
|
||||
<span v-else-if="activity.average_speed && Number(units) === 2"><b>{{ formatAverageSpeedImperial(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsMph") }}</b></span>
|
||||
<span v-if="activity.average_speed && Number(units) === 1"><b>{{
|
||||
formatAverageSpeedMetric(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsKmH")
|
||||
}}</b></span>
|
||||
<span v-else-if="activity.average_speed && Number(units) === 2"><b>{{
|
||||
formatAverageSpeedImperial(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsMph")
|
||||
}}</b></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.max_speed">
|
||||
@@ -55,8 +62,12 @@
|
||||
{{ $t("activityBellowMPillsComponent.labelMaxSpeed") }}
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="activity.max_speed && Number(units) === 1"><b>{{ formatAverageSpeedMetric(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsKmH") }}</b></span>
|
||||
<span v-else-if="activity.max_speed && Number(units) === 2"><b>{{ formatAverageSpeedImperial(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsMph") }}</b></span>
|
||||
<span v-if="activity.max_speed && Number(units) === 1"><b>{{
|
||||
formatAverageSpeedMetric(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsKmH")
|
||||
}}</b></span>
|
||||
<span v-else-if="activity.max_speed && Number(units) === 2"><b>{{
|
||||
formatAverageSpeedImperial(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsMph")
|
||||
}}</b></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.total_elapsed_time">
|
||||
@@ -82,7 +93,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleHeartRate") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'hr'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'hr'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_hr">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgHeartRate") }}
|
||||
@@ -99,6 +111,11 @@
|
||||
<b>{{ activity.max_hr }}{{ ' ' + $t("generalItems.unitsBpm") }}</b>
|
||||
</span>
|
||||
</div>
|
||||
<BarChartComponent v-if="Object.values(hrZones).length > 0 && hrPresent"
|
||||
:labels="getHrBarChartData(hrZones, t).labels" :values="getHrBarChartData(hrZones, t).values"
|
||||
:barColors="getHrBarChartData(hrZones, t).barColors"
|
||||
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
|
||||
:title="$t('activityMandAbovePillsComponent.labelHRZones')" />
|
||||
<hr>
|
||||
</div>
|
||||
<!-- Power values -->
|
||||
@@ -106,7 +123,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitlePower") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'power'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'power'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_power">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgPower") }}
|
||||
@@ -141,7 +159,8 @@
|
||||
<span class="fw-normal" v-else>
|
||||
{{ $t("activityBellowMPillsComponent.subTitleStrokeRate") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'cad'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'cad'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_cad">
|
||||
<span v-if="!activityTypeIsSwimming(activity)">
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgCadence") }}
|
||||
@@ -171,7 +190,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleElevation") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'ele'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'ele'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.elevation_gain">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelElevationGain") }}
|
||||
@@ -197,8 +217,11 @@
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<!-- sets -->
|
||||
<ActivityWorkoutStepsComponent :activity="activity" :activityActivityWorkoutSteps="activityActivityWorkoutSteps" :units="units" :activityActivityExerciseTitles="activityActivityExerciseTitles" :activityActivitySets="activityActivitySets" v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0"/>
|
||||
<!-- sets -->
|
||||
<ActivityWorkoutStepsComponent :activity="activity" :activityActivityWorkoutSteps="activityActivityWorkoutSteps"
|
||||
:units="units" :activityActivityExerciseTitles="activityActivityExerciseTitles"
|
||||
:activityActivitySets="activityActivitySets"
|
||||
v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -209,50 +232,51 @@ import { useI18n } from "vue-i18n";
|
||||
import ActivityLapsComponent from "@/components/Activities/ActivityLapsComponent.vue";
|
||||
import ActivityStreamsLineChartComponent from "@/components/Activities/ActivityStreamsLineChartComponent.vue";
|
||||
import ActivityWorkoutStepsComponent from "@/components/Activities/ActivityWorkoutStepsComponent.vue";
|
||||
import BarChartComponent from '@/components/GeneralComponents/BarChartComponent.vue';
|
||||
// Import Notivue push
|
||||
import { push } from "notivue";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
// Import the utils
|
||||
import { getHrBarChartData } from "@/utils/chartUtils";
|
||||
import { formatPaceMetric, formatPaceImperial, formatPaceSwimMetric, formatPaceSwimImperial, formatAverageSpeedMetric, formatAverageSpeedImperial, activityTypeIsSwimming } from "@/utils/activityUtils";
|
||||
import { formatSecondsToMinutes } from "@/utils/dateTimeUtils";
|
||||
import {
|
||||
metersToFeet,
|
||||
metersToFeet,
|
||||
} from "@/utils/unitsUtils";
|
||||
|
||||
// Define props
|
||||
const props = defineProps({
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activityActivityLaps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityWorkoutSteps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityStreams: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
activityActivityExerciseTitles: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivitySets: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activityActivityLaps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityWorkoutSteps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityStreams: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
activityActivityExerciseTitles: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivitySets: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Setup composables and reactive data
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
const hrPresent = ref(false);
|
||||
const powerPresent = ref(false);
|
||||
const elePresent = ref(false);
|
||||
@@ -260,74 +284,85 @@ const cadPresent = ref(false);
|
||||
const velPresent = ref(false);
|
||||
const pacePresent = ref(false);
|
||||
const formattedPace = ref(null);
|
||||
const hrZones = ref({
|
||||
zone_1: {},
|
||||
zone_2: {},
|
||||
zone_3: {},
|
||||
zone_4: {},
|
||||
zone_5: {},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
// Check if the activity has the streams
|
||||
for (let i = 0; i < props.activityActivityStreams.length; i++) {
|
||||
if (props.activityActivityStreams[i].stream_type === 1) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 2) {
|
||||
powerPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 3) {
|
||||
cadPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 4) {
|
||||
elePresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (
|
||||
props.activity.activity_type === 4 ||
|
||||
props.activity.activity_type === 5 ||
|
||||
props.activity.activity_type === 6 ||
|
||||
props.activity.activity_type === 7 ||
|
||||
props.activity.activity_type === 27
|
||||
) {
|
||||
velPresent.value = true;
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (
|
||||
props.activity.activity_type !== 4 &&
|
||||
props.activity.activity_type !== 5 &&
|
||||
props.activity.activity_type !== 6 &&
|
||||
props.activity.activity_type !== 7 &&
|
||||
props.activity.activity_type !== 27
|
||||
) {
|
||||
pacePresent.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
push.error(
|
||||
`${t("activityMandAbovePillsComponent.errorMessageProcessingActivityStreams")} - ${error}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
// Check if the activity has the streams
|
||||
for (let i = 0; i < props.activityActivityStreams.length; i++) {
|
||||
if (props.activityActivityStreams[i].stream_type === 1) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 2) {
|
||||
powerPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 3) {
|
||||
cadPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 4) {
|
||||
elePresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (
|
||||
props.activity.activity_type === 4 ||
|
||||
props.activity.activity_type === 5 ||
|
||||
props.activity.activity_type === 6 ||
|
||||
props.activity.activity_type === 7 ||
|
||||
props.activity.activity_type === 27
|
||||
) {
|
||||
velPresent.value = true;
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (
|
||||
props.activity.activity_type !== 4 &&
|
||||
props.activity.activity_type !== 5 &&
|
||||
props.activity.activity_type !== 6 &&
|
||||
props.activity.activity_type !== 7 &&
|
||||
props.activity.activity_type !== 27
|
||||
) {
|
||||
pacePresent.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
hrZones.value = props.activityActivityStreams.find(stream => stream.hr_zone_percentages).hr_zone_percentages || {};
|
||||
if (Object.keys(hrZones.value).length > 0) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
push.error(
|
||||
`${t("activityMandAbovePillsComponent.errorMessageProcessingActivityStreams")} - ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
activityTypeIsSwimming(props.activity) ||
|
||||
props.activity.activity_type === 13
|
||||
) {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceSwimMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceSwimImperial(props.activity.pace));
|
||||
}
|
||||
} else {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceImperial(props.activity.pace));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
}
|
||||
try {
|
||||
if (
|
||||
activityTypeIsSwimming(props.activity) ||
|
||||
props.activity.activity_type === 13
|
||||
) {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceSwimMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceSwimImperial(props.activity.pace));
|
||||
}
|
||||
} else {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceImperial(props.activity.pace));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -57,10 +57,13 @@
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams"
|
||||
v-if="graphSelection === 'pace' && pacePresent" />
|
||||
<BarChartComponent v-if="Object.values(hrZones).length > 0 && graphSelection === 'hrZones' && hrPresent" :labels="getHrBarChartData().labels"
|
||||
:values="getHrBarChartData().values" :barColors="getHrBarChartData().barColors"
|
||||
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
|
||||
:title="$t('activityMandAbovePillsComponent.labelHRZones')" />
|
||||
<BarChartComponent
|
||||
v-if="Object.values(hrZones).length > 0 && graphSelection === 'hrZones' && hrPresent"
|
||||
:labels="getHrBarChartData(hrZones, t).labels"
|
||||
:values="getHrBarChartData(hrZones, t).values"
|
||||
:barColors="getHrBarChartData(hrZones, t).barColors"
|
||||
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
|
||||
:title="$t('activityMandAbovePillsComponent.labelHRZones')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,9 +94,10 @@ import ActivityStreamsLineChartComponent from "@/components/Activities/ActivityS
|
||||
import ActivityWorkoutStepsComponent from "@/components/Activities/ActivityWorkoutStepsComponent.vue";
|
||||
import BarChartComponent from '@/components/GeneralComponents/BarChartComponent.vue';
|
||||
import { activityTypeIsSwimming } from "@/utils/activityUtils";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
// Import Notivue push
|
||||
import { push } from "notivue";
|
||||
// Import the utils
|
||||
import { getHrBarChartData } from "@/utils/chartUtils";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -129,7 +133,6 @@ const props = defineProps({
|
||||
|
||||
// Composables
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Reactive state
|
||||
const graphSelection = ref("hr");
|
||||
@@ -141,11 +144,11 @@ const cadPresent = ref(false);
|
||||
const velPresent = ref(false);
|
||||
const pacePresent = ref(false);
|
||||
const hrZones = ref({
|
||||
zone_1: {},
|
||||
zone_2: {},
|
||||
zone_3: {},
|
||||
zone_4: {},
|
||||
zone_5: {},
|
||||
zone_1: {},
|
||||
zone_2: {},
|
||||
zone_3: {},
|
||||
zone_4: {},
|
||||
zone_5: {},
|
||||
});
|
||||
|
||||
// Methods
|
||||
@@ -208,8 +211,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
hrZones.value = props.activityActivityStreams.find(stream => stream.hr_zone_percentages).hr_zone_percentages || {};
|
||||
if (Object.keys(hrZones.value).length > 0) {
|
||||
hrPresent.value = true;
|
||||
@@ -226,26 +227,4 @@ onMounted(async () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function getZoneColor(index) {
|
||||
// Example colors for 5 HR zones
|
||||
const colors = [
|
||||
'#1e90ff', // Zone 1: blue
|
||||
'#28a745', // Zone 2: green
|
||||
'#ffc107', // Zone 3: yellow
|
||||
'#fd7e14', // Zone 4: orange
|
||||
'#dc3545', // Zone 5: red
|
||||
];
|
||||
return colors[index] || '#000';
|
||||
}
|
||||
|
||||
function getHrBarChartData() {
|
||||
const zones = Object.values(hrZones.value);
|
||||
return {
|
||||
labels: zones.map((z, i) => `${t('activityMandAbovePillsComponent.labelGraphHRZone')} ${i + 1} (${z.hr || ''})`),
|
||||
// values: zones.map(z => `${z.percent ?? 0}%`),
|
||||
values: zones.map(z => z.percent ?? 0),
|
||||
barColors: zones.map((_, i) => getZoneColor(i)),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
21
frontend/app/src/utils/chartUtils.js
Normal file
21
frontend/app/src/utils/chartUtils.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export function getZoneColor(index) {
|
||||
// Example colors for 5 HR zones
|
||||
const colors = [
|
||||
'#1e90ff', // Zone 1: blue
|
||||
'#28a745', // Zone 2: green
|
||||
'#ffc107', // Zone 3: yellow
|
||||
'#fd7e14', // Zone 4: orange
|
||||
'#dc3545', // Zone 5: red
|
||||
];
|
||||
return colors[index] || '#000';
|
||||
}
|
||||
|
||||
export function getHrBarChartData(hrZones, t) {
|
||||
const zones = Object.values(hrZones);
|
||||
return {
|
||||
labels: zones.map((z, i) => `${t('activityMandAbovePillsComponent.labelGraphHRZone')} ${i + 1} (${z.hr || ''})`),
|
||||
// values: zones.map(z => `${z.percent ?? 0}%`),
|
||||
values: zones.map(z => z.percent ?? 0),
|
||||
barColors: zones.map((_, i) => getZoneColor(i)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user