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. 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. db: The database session or connection used to fetch user details.
Returns: 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: Notes:
- Heart rate zones are calculated using the formula: max_heart_rate = 220 - age. - 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. - 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_3) & (hr_values < zone_4)),
np.sum(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 # Calculate zone HR boundaries for display
zone_hr = { zone_hr = {
@@ -434,11 +452,31 @@ def transform_activity_streams_hr(activity_stream, activity, db):
"zone_5": f">= {int(zone_4)}", "zone_5": f">= {int(zone_4)}",
} }
activity_stream.hr_zone_percentages = { activity_stream.hr_zone_percentages = {
"zone_1": {"percent": zone_percentages[0], "hr": zone_hr["zone_1"]}, "zone_1": {
"zone_2": {"percent": zone_percentages[1], "hr": zone_hr["zone_2"]}, "percent": zone_percentages[0],
"zone_3": {"percent": zone_percentages[2], "hr": zone_hr["zone_3"]}, "hr": zone_hr["zone_1"],
"zone_4": {"percent": zone_percentages[3], "hr": zone_hr["zone_4"]}, "time_seconds": zone_time_seconds[0],
"zone_5": {"percent": zone_percentages[4], "hr": zone_hr["zone_5"]}, },
"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 return activity_stream

View File

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

View File

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

View File

@@ -31,6 +31,10 @@ const props = defineProps({
datalabelsFormatter: { datalabelsFormatter: {
type: Function, type: Function,
default: null 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) { export function getZoneColor(index) {
// Example colors for 5 HR zones // Example colors for 5 HR zones
const colors = [ 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}%`),
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)
} }
} }