diff --git a/backend/app/activities/activity_streams/crud.py b/backend/app/activities/activity_streams/crud.py index cc5db12d4..64cdf302d 100644 --- a/backend/app/activities/activity_streams/crud.py +++ b/backend/app/activities/activity_streams/crud.py @@ -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 diff --git a/frontend/app/src/components/Activities/ActivityBellowMPillsComponent.vue b/frontend/app/src/components/Activities/ActivityBellowMPillsComponent.vue index 4c7a8614a..7c85bfbf6 100644 --- a/frontend/app/src/components/Activities/ActivityBellowMPillsComponent.vue +++ b/frontend/app/src/components/Activities/ActivityBellowMPillsComponent.vue @@ -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')" />
diff --git a/frontend/app/src/components/Activities/ActivityMandAbovePillsComponent.vue b/frontend/app/src/components/Activities/ActivityMandAbovePillsComponent.vue index 7df75802e..26d57c8e4 100644 --- a/frontend/app/src/components/Activities/ActivityMandAbovePillsComponent.vue +++ b/frontend/app/src/components/Activities/ActivityMandAbovePillsComponent.vue @@ -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')" /> diff --git a/frontend/app/src/components/GeneralComponents/BarChartComponent.vue b/frontend/app/src/components/GeneralComponents/BarChartComponent.vue index 7a7c805eb..8ec4b9b3b 100644 --- a/frontend/app/src/components/GeneralComponents/BarChartComponent.vue +++ b/frontend/app/src/components/GeneralComponents/BarChartComponent.vue @@ -31,6 +31,10 @@ const props = defineProps({ datalabelsFormatter: { type: Function, default: null + }, + timeSeconds: { + type: Array, + default: () => [] } }) diff --git a/frontend/app/src/utils/chartUtils.js b/frontend/app/src/utils/chartUtils.js index 83e3bf651..e86a486f5 100644 --- a/frontend/app/src/utils/chartUtils.js +++ b/frontend/app/src/utils/chartUtils.js @@ -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) } }