Add time duration display to heart rate zones

- Backend: Calculate time_seconds for each HR zone based on waypoint ratio
- Frontend: Display both percentage and time (e.g., '25% (15m)') in HR zone charts
- Update BarChartComponent to support timeSeconds prop
- Use activity total_timer_time for accurate time calculation
This commit is contained in:
Boris Stäheli
2025-12-13 22:42:23 +01:00
parent b0bc916c44
commit 317922d416
5 changed files with 54 additions and 8 deletions

View File

@@ -424,6 +424,15 @@ def transform_activity_streams_hr(activity_stream, activity, db):
np.sum(hr_values >= zone_4),
]
zone_percentages = [round((count / total) * 100, 2) for count in zone_counts]
# Calculate time in seconds for each zone
# Use the same logic as percentage: distribute total_timer_time based on waypoint ratio
if hasattr(activity, 'total_timer_time') and activity.total_timer_time and total > 0:
total_time_seconds = activity.total_timer_time
zone_time_seconds = [int((count / total) * total_time_seconds) for count in zone_counts]
else:
# Fallback: assume waypoints represent equal time intervals
zone_time_seconds = [int(count) for count in zone_counts]
# Calculate zone HR boundaries for display
zone_hr = {
@@ -434,11 +443,11 @@ 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

@@ -141,7 +141,16 @@
:labels="getHrBarChartData(hrZones, t).labels"
:values="getHrBarChartData(hrZones, t).values"
:barColors="getHrBarChartData(hrZones, t).barColors"
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
:timeSeconds="getHrBarChartData(hrZones, t).timeSeconds"
:datalabelsFormatter="
(value, context) => {
const timeSeconds = getHrBarChartData(hrZones, t).timeSeconds[context.dataIndex]
const hours = Math.floor(timeSeconds / 3600)
const minutes = Math.floor((timeSeconds % 3600) / 60)
const timeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
return `${Math.round(value)}% (${timeStr})`
}
"
:title="$t('activityMandAbovePillsComponent.labelHRZones')"
/>
<hr />

View File

@@ -123,7 +123,16 @@
:labels="getHrBarChartData(hrZones, t).labels"
:values="getHrBarChartData(hrZones, t).values"
:barColors="getHrBarChartData(hrZones, t).barColors"
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
:timeSeconds="getHrBarChartData(hrZones, t).timeSeconds"
:datalabelsFormatter="
(value, context) => {
const timeSeconds = getHrBarChartData(hrZones, t).timeSeconds[context.dataIndex]
const hours = Math.floor(timeSeconds / 3600)
const minutes = Math.floor((timeSeconds % 3600) / 60)
const timeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
return `${Math.round(value)}% (${timeStr})`
}
"
:title="$t('activityMandAbovePillsComponent.labelHRZones')"
/>
</div>

View File

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

View File

@@ -1,3 +1,17 @@
/**
* 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").
*/
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`
}
export function getZoneColor(index) {
// Example colors for 5 HR zones
const colors = [
@@ -18,6 +32,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)
}
}