Merge branch 'pr/442' into pre-release

This commit is contained in:
João Vitória Silva
2025-12-22 15:37:42 +00:00
5 changed files with 102 additions and 18 deletions

View File

@@ -369,7 +369,7 @@ def transform_activity_streams_hr(activity_stream, activity, db):
activity: The activity object associated with the stream, used to retrieve the user ID.
db: The database session or connection used to fetch user details.
Returns:
The activity stream object with an added 'hr_zone_percentages' attribute, which contains the percentage of time spent in each heart rate zone and their respective HR boundaries. If waypoints or user details are missing, returns the original activity stream unchanged.
The activity stream object with an added 'hr_zone_percentages' attribute, which contains the percentage of time spent in each heart rate zone and their respective HR boundaries. If waypoi[...]
Notes:
- Heart rate zones are calculated using the formula: max_heart_rate = 220 - age.
- The function expects waypoints to be a list of dicts with an "hr" key.
@@ -423,7 +423,25 @@ def transform_activity_streams_hr(activity_stream, activity, db):
np.sum((hr_values >= zone_3) & (hr_values < zone_4)),
np.sum(hr_values >= zone_4),
]
zone_percentages = [round((count / total) * 100, 2) for count in zone_counts]
zone_percentages = [
round((count / total) * 100, 2) for count in zone_counts
]
# Calculate time in seconds for each zone using the percentage
# of total_timer_time
has_timer_time = (
hasattr(activity, "total_timer_time")
and activity.total_timer_time
)
if has_timer_time:
total_time_seconds = activity.total_timer_time
zone_time_seconds = [
int((percent / 100) * total_time_seconds)
for percent in zone_percentages
]
else:
# Fallback: no time calculation possible
zone_time_seconds = [0, 0, 0, 0, 0]
# Calculate zone HR boundaries for display
zone_hr = {
@@ -434,11 +452,31 @@ def transform_activity_streams_hr(activity_stream, activity, db):
"zone_5": f">= {int(zone_4)}",
}
activity_stream.hr_zone_percentages = {
"zone_1": {"percent": zone_percentages[0], "hr": zone_hr["zone_1"]},
"zone_2": {"percent": zone_percentages[1], "hr": zone_hr["zone_2"]},
"zone_3": {"percent": zone_percentages[2], "hr": zone_hr["zone_3"]},
"zone_4": {"percent": zone_percentages[3], "hr": zone_hr["zone_4"]},
"zone_5": {"percent": zone_percentages[4], "hr": zone_hr["zone_5"]},
"zone_1": {
"percent": zone_percentages[0],
"hr": zone_hr["zone_1"],
"time_seconds": zone_time_seconds[0],
},
"zone_2": {
"percent": zone_percentages[1],
"hr": zone_hr["zone_2"],
"time_seconds": zone_time_seconds[1],
},
"zone_3": {
"percent": zone_percentages[2],
"hr": zone_hr["zone_3"],
"time_seconds": zone_time_seconds[2],
},
"zone_4": {
"percent": zone_percentages[3],
"hr": zone_hr["zone_4"],
"time_seconds": zone_time_seconds[3],
},
"zone_5": {
"percent": zone_percentages[4],
"hr": zone_hr["zone_5"],
"time_seconds": zone_time_seconds[4],
},
}
return activity_stream

View File

@@ -138,10 +138,13 @@
</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)}%`"
:labels="hrChartData.labels"
:values="hrChartData.values"
:barColors="hrChartData.barColors"
:timeSeconds="hrChartData.timeSeconds"
:datalabelsFormatter="
(value, context) => formatHrZoneLabel(value, hrChartData.timeSeconds[context.dataIndex])
"
:title="$t('activityMandAbovePillsComponent.labelHRZones')"
/>
<hr />
@@ -346,6 +349,9 @@ const pacePresent = ref(false)
const formattedPace = ref(null)
const hrZones = ref({})
// Computed properties
const hrChartData = computed(() => getHrBarChartData(hrZones.value, t))
onMounted(async () => {
try {
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {

View File

@@ -120,10 +120,13 @@
/>
<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)}%`"
:labels="hrChartData.labels"
:values="hrChartData.values"
:barColors="hrChartData.barColors"
:timeSeconds="hrChartData.timeSeconds"
:datalabelsFormatter="
(value, context) => formatHrZoneLabel(value, hrChartData.timeSeconds[context.dataIndex])
"
:title="$t('activityMandAbovePillsComponent.labelHRZones')"
/>
</div>
@@ -167,7 +170,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
// Importing the components
import ActivityLapsComponent from '@/components/Activities/ActivityLapsComponent.vue'
@@ -186,7 +189,7 @@ import {
// Import Notivue push
import { push } from 'notivue'
// Import the utils
import { getHrBarChartData } from '@/utils/chartUtils'
import { getHrBarChartData, formatHrZoneLabel } from '@/utils/chartUtils'
// Props
const props = defineProps({
@@ -234,6 +237,9 @@ const velPresent = ref(false)
const pacePresent = ref(false)
const hrZones = ref({})
// Computed properties
const hrChartData = computed(() => getHrBarChartData(hrZones.value, t))
// Methods
function selectGraph(type) {
graphSelection.value = type

View File

@@ -31,6 +31,10 @@ const props = defineProps({
datalabelsFormatter: {
type: Function,
default: null
},
timeSeconds: {
type: Array,
default: () => []
}
})

View File

@@ -1,3 +1,32 @@
/**
* Formats seconds into a human-readable duration string.
* @param {number} seconds - The total number of seconds to format.
* @returns {string} Formatted duration string (e.g., "2h 30m" or "45m").
*/
export function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}h ${minutes}m`
}
return `${minutes}m`
}
/**
* Formats HR zone label with percentage and optional time duration.
* @param {number} value - Percentage value
* @param {number|null} timeSeconds - Time in seconds (0 or null means no time)
* @returns {string} Formatted label (e.g., "25%" or "25% (15m)")
*/
export function formatHrZoneLabel(value, timeSeconds) {
const percentage = `${Math.round(value)}%`
if (!timeSeconds || timeSeconds === 0) {
return percentage
}
const timeStr = formatDuration(timeSeconds)
return `${percentage} (${timeStr})`
}
export function getZoneColor(index) {
// Example colors for 5 HR zones
const colors = [
@@ -18,6 +47,7 @@ export function getHrBarChartData(hrZones, t) {
),
// values: zones.map(z => `${z.percent ?? 0}%`),
values: zones.map((z) => z.percent ?? 0),
barColors: zones.map((_, i) => getZoneColor(i))
barColors: zones.map((_, i) => getZoneColor(i)),
timeSeconds: zones.map((z) => z.time_seconds ?? 0)
}
}