Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore Invalid HR on HR Zone #68

Merged
merged 7 commits into from
Dec 5, 2023
121 changes: 77 additions & 44 deletions src/components/HeartRateZoneGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import HeartRateZoneBar from './HeartRateZoneBar.vue'
</script>

<template>
<div class="container pt-2 pb-3">
<div class="container">
<div class="row">
<div
class="col text-start collapsible"
Expand Down Expand Up @@ -43,12 +43,13 @@ import HeartRateZoneBar from './HeartRateZoneBar.vue'
<span>Common formula: </span><br />
<b>Max HR = 220 - Age</b>
"
>&nbsp;</i
></span>
>&nbsp;
</i>
</span>
</div>
</div>
</div>
<div class="row collapse show" id="hrzone-graph-content">
<div class="row collapse show pb-3" id="hrzone-graph-content">
<div class="col-12 pt-2" v-for="hrZone in hrZones" :key="hrZone.zone">
<HeartRateZoneBar
:zone="hrZone.zone"
Expand Down Expand Up @@ -265,20 +266,24 @@ export default {

// Process each data point and calculate heart rate zone and total time
sessions.forEach((session) => {
if (!session.records) return
if (session.records == null) return
for (let i = 0; i < session.records.length - 1; i++) {
const entry = session.records[i]
const nextEntry = session.records[i + 1]
if (entry == null || entry.heartRate == null) continue

const hrZoneIndex = this.getHeartRateZoneIndex(entry.heartRate || 0)
const nextHrZoneIndex = this.getHeartRateZoneIndex(nextEntry.heartRate || 0)
let { nextEntry, nextIndex } = this.getNextValidEntry(session, entry, i)
i = nextIndex // skip loop to latest valid entry

const hrZoneIndex = this.getHeartRateZoneIndex(entry.heartRate)
const nextHrZoneIndex = this.getHeartRateZoneIndex(nextEntry.heartRate ?? 0) // should be valid, but tslinter can't check

if (entry.timestamp == null || nextEntry.timestamp == null) continue

const timestamp1 = new Date(entry.timestamp || nextEntry.timestamp)
const timestamp2 = new Date(nextEntry.timestamp || nextEntry.timestamp)
const timestamp1 = new Date(entry.timestamp)
const timestamp2 = new Date(nextEntry.timestamp)
let secondsDiff: number = (timestamp2.valueOf() - timestamp1.valueOf()) / 1000
if (secondsDiff > 30) secondsDiff = 1
if (secondsDiff > 30 || secondsDiff < 0) secondsDiff = 1
if (entry == nextEntry) secondsDiff = 1

totalSeconds += secondsDiff

Expand All @@ -296,10 +301,10 @@ export default {
// Calculate percentage of time in each zone and assign to hr zone
const zonePercentages: any = {}
for (const [zoneIndex, zoneSeconds] of zoneTotals.entries()) {
const percentage = (zoneSeconds / totalSeconds) * 100
zonePercentages[zoneIndex] = percentage

if (this.hrZones[zoneIndex]) {
const percentage = (zoneSeconds / totalSeconds) * 100
zonePercentages[zoneIndex] = percentage

this.hrZones[zoneIndex].prosen = percentage || 0
this.hrZones[zoneIndex].timeInSecond = zoneSeconds
}
Expand Down Expand Up @@ -354,21 +359,27 @@ export default {
let totalSeconds = 0

// Process each data point and calculate heart rate zone and total time
// TODO optimize calculation
sessions.forEach((session) => {
if (!session.records) return
if (session.records == null) return
console.time('totalSteps')
for (let i = 0; i < session.records.length - 1; i++) {
const entry = session.records[i]
const nextEntry = session.records[i + 1]
if (entry == null || entry.heartRate == null) continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually entry will never be null, it's guaranteed. but this is okay too, no worries.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually entry will never be null, it's guaranteed. but this is okay too, no worries.

i think this leftover from previous one that Record can be null (cause linter red), i'll check more on this


const hrZoneIndex = this.getHeartRateZoneIndex(entry.heartRate || 0)
const nextHrZoneIndex = this.getHeartRateZoneIndex(nextEntry.heartRate || 0)
let { nextEntry, nextIndex } = this.getNextValidEntry(session, entry, i)
i = nextIndex // skip loop to latest valid entry

const hrZoneIndex = this.getHeartRateZoneIndex(entry.heartRate)
const nextHrZoneIndex = this.getHeartRateZoneIndex(nextEntry.heartRate ?? 0) // should be valid, but tslinter can't check

if (entry.timestamp == null || nextEntry.timestamp == null) continue

const timestamp1 = new Date(entry.timestamp || nextEntry.timestamp)
const timestamp2 = new Date(nextEntry.timestamp || nextEntry.timestamp)
const timestamp1 = new Date(entry.timestamp)
const timestamp2 = new Date(nextEntry.timestamp)
let secondsDiff: number = (timestamp2.valueOf() - timestamp1.valueOf()) / 1000
if (secondsDiff > 30) secondsDiff = 1
if (secondsDiff > 30 || secondsDiff < 0) secondsDiff = 1
if (entry == nextEntry) secondsDiff = 1

totalSeconds += secondsDiff

Expand All @@ -381,29 +392,41 @@ export default {
// calculate the bpm step
const zonesInvolved = this.determineZonesInvolved(hrZoneIndex, nextHrZoneIndex)
const totalSteps = zonesInvolved.reduce((total, zoneIndex) => {
const start = Math.min(
Math.max(this.hrZones[zoneIndex].minmax[0], entry.heartRate || 0),
this.hrZones[zoneIndex].minmax[1]
)
const end = Math.min(
Math.max(this.hrZones[zoneIndex].minmax[0], nextEntry.heartRate || 0),
this.hrZones[zoneIndex].minmax[1]
)
const start =
zoneIndex == -1
? entry.heartRate ?? 0
: Math.min(
Math.max(this.hrZones[zoneIndex].minmax[0], entry.heartRate ?? 0),
this.hrZones[zoneIndex].minmax[1]
)
const end =
zoneIndex == -1
? nextEntry.heartRate ?? 0
: Math.min(
Math.max(this.hrZones[zoneIndex].minmax[0], nextEntry.heartRate ?? 0),
this.hrZones[zoneIndex].minmax[1]
)
return 1 + total + (Math.max(end, start) - Math.min(end, start))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be simplified though

return 1 + total + Math.abs(start-end)

btw one question pls, if zoneIndex == -1 what will happen then? Can you give a little illustration regarding this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entry.heartRate = 50
nextEntry.heartRate = 200

assume 50 is below zone 1, and 200 is above zone 5, it will belong to the zone who has (200-50) 150bpm in it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe more real world case:

entry.heartRate = 50 (assume not belong to zone)
entry.heartRate = 90 (assume zone 1)

it will add 40 steps then distribute them evenly based on elapsed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entry.heartRate = 50
nextEntry.heartRate = 200

It will do transition 50 (inclusive) to 200 (inclusive) with each value added time to zone (1/151 * deltaTime)
roughly figure like this

200 BPM = 1/151 * deltaTime -> Zone 5
...
50 BPM = 1/151 * deltaTime -> No Zone

because Zone 5 is 90% to Infinity, therefore 200+ still counted as Zone 5.
because Zone 1 is 50%-60%, therefore 50 didn't counted as Zone 1 and discarded.

}, 0)

// calculate the fraction or delta
for (let j = 0; j < zonesInvolved.length; j++) {
const zIndex = zonesInvolved[j]
zoneTotals[zIndex] = zoneTotals[zIndex] || 0
const start = Math.min(
Math.max(this.hrZones[zIndex].minmax[0], entry.heartRate || 0),
this.hrZones[zIndex].minmax[1]
)
const end = Math.min(
Math.max(this.hrZones[zIndex].minmax[0], nextEntry.heartRate || 0),
this.hrZones[zIndex].minmax[1]
)
const start =
zIndex == -1
? entry.heartRate ?? 0
: Math.min(
Math.max(this.hrZones[zIndex].minmax[0], entry.heartRate || 0),
this.hrZones[zIndex].minmax[1]
)
const end =
zIndex == -1
? nextEntry.heartRate ?? 0
: Math.min(
Math.max(this.hrZones[zIndex].minmax[0], nextEntry.heartRate || 0),
this.hrZones[zIndex].minmax[1]
)
const fraction = 1 + (Math.max(end, start) - Math.min(end, start))
zoneTotals[zIndex] += secondsDiff * (fraction / totalSteps)
}
Expand All @@ -412,15 +435,17 @@ export default {
zoneTotals[hrZoneIndex] += secondsDiff
}
}
console.timeEnd('totalSteps')
})

// Calculate percentage of time in each zone and assign to hr zone
const zonePercentages: any = {}
const invalidTotalSeconds = zoneTotals[-1] ?? 0
for (const [zoneIndex, zoneSeconds] of zoneTotals.entries()) {
const percentage = (zoneSeconds / totalSeconds) * 100
zonePercentages[zoneIndex] = percentage

if (this.hrZones[zoneIndex]) {
const percentage = (zoneSeconds / (totalSeconds - invalidTotalSeconds)) * 100
zonePercentages[zoneIndex] = percentage

this.hrZones[zoneIndex].prosen = percentage || 0
this.hrZones[zoneIndex].timeInSecond = zoneSeconds
}
Expand All @@ -438,6 +463,16 @@ export default {

console.log(`> Total Time: ${totalSeconds.toFixed(2)} seconds`)
},
getNextValidEntry(session: Session, currentEntry: Record, currentIndex: number) {
// findout next record with valid HR
for (let index = currentIndex + 1; index < session.records.length; index++) {
const r = session.records[index]
// r.heartRate = (Math.floor(Math.random() * (10 + 1)) + 1) % 2 == 0 ? r.heartRate : 55 // Test random null HR
if (r.heartRate != null) return { nextEntry: r, nextIndex: index }
}
// no next entry, use current entry as last comparator
return { nextEntry: currentEntry, nextIndex: session.records.length - 1 }
},
// get hr this.hrZones index based on hr
getHeartRateZoneIndex(heartRate: number) {
for (const [i, d] of this.hrZones.entries()) {
Expand All @@ -449,16 +484,14 @@ export default {
},
// get hrzones involved between calculate transition 2 data hr
determineZonesInvolved(startIndex: number, endIndex: number) {
if (startIndex === -1 || endIndex === -1) {
return []
}

const direction = startIndex < endIndex ? 1 : -1
const zonesInvolved = []

if (startIndex == -1) zonesInvolved.push(-1)
for (let i = startIndex; i !== endIndex + direction; i += direction) {
zonesInvolved.push(i)
}
if (endIndex == -1) zonesInvolved.push(-1)

return zonesInvolved
}
Expand Down
Loading