-
-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from 5 commits
17ccf94
7006df2
9ccba06
a0b9685
128f69c
21b2bb8
8e49542
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -43,12 +43,13 @@ import HeartRateZoneBar from './HeartRateZoneBar.vue' | |
<span>Common formula: </span><br /> | ||
<b>Max HR = 220 - Age</b> | ||
" | ||
> </i | ||
></span> | ||
> | ||
</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" | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
} | ||
|
@@ -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 | ||
|
||
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 | ||
|
||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. entry.heartRate = 50 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) it will add 40 steps then distribute them evenly based on elapsed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It will do transition 50 (inclusive) to 200 (inclusive) with each value added time to zone
because Zone 5 is 90% to Infinity, therefore 200+ still counted as Zone 5. |
||
}, 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) | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -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()) { | ||
|
@@ -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 | ||
} | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this leftover from previous one that Record can be null (cause linter red), i'll check more on this