I created a chart to track P/HRR with the following code:
{
let mhr = 201
let rhr = 57
let hr = icu.streams.fixed_heartrate
let power = icu.streams.fixed_watts
for (let i = 0; i < data.length; i++) data[i] = power[i] / (((hr[i] - rhr) / (mhr - rhr))*100)
}
Can you help me with accessing the maxHR and restHR? How do I get those variables ?
I also tried to re-use the same code for an interval field, but couldn’t get it to work there. Does it work the same ?
{
let mhr = activity.athlete_max_hr
let rhr = activity.icu_resting_hr
let hr = icu.streams.fixed_heartrate
let power = icu.streams.fixed_watts
let tot = 0, c = 0
for (let i = interval.start_index; i < interval.end_index; i++) {
let v = power[i] / (((hr[i] - rhr) / (mhr - rhr))*100)
if (v) {
tot += v
++c
}
}
c ? tot/c : null
}
This is great, how is it not getting more traction?
I ownder if there is a guide somewhere to how the individual streams are called. I would like to build, for example, an estimated power on climbs from VAM and compare it to the actual power.
I pulled all the extension posts into a guide. I hope to edit that to provide more of an overview soon. But it is always “do I write some more docs” vs “implement new features”
I just managed to do exactly what I’ve been looking to do for a long time, thanks again @david for the feature. The integration with the custom charts is amazing!
I implemented the classic VAM algorithm to double-check the power meter on climbs and/or get indication of drafting/winds:
Relative power (watts/kg) = VAM (metres/hour) / (200 + 10 × % grade) [1]
Indeed, it works, and here is the result from a recent climb:
You can get the athlete’s weight at the time of the activity from activity.icu_weight (in kg).
If your custom stream is calculated from other streams (and the activity etc.) and does not need to access the fit file then doing Actions → Re-analyse will recompute it. That will let you keep the intervals.
I will sort out reprocess file losing the intervals. Been meaning to do that.
@david I see now (and from your earlier replies to this topic, duh) that there’s an activity object directly available for the script. I assumed you had to fetch them from the icu object like you do for streams. It’s not super explicit from the original announcement:
Are any other fields in ActivityJsData also brought into the script scope? Or is this activity object something else?
It is the same activity object. When I started this there wasn’t an ‘icu’ object but then I realised that was a better approach because it allows lazy loading of requested stuff. Future things will all be on the icu object.
You can now choose to keep intervals when re-processing an activity file both for Actions → Re-process and when bulk editing from the activity list view:
VAM is a stream but it is computed client side. If you search for “VAM” in the custom streams search dialog Kosio Varbenov’s calculated power comes up.
Gave Di2/eTap/EPS streams a shot. I have something that works for me and in theory it should be universal, but if it doesn’t work for you, try and share a FIT file and I might give it a look. No promises though. Search for “Di2” in custom streams and they should all show up.
This only works if FIT is available, since it needs the FIT gear_change_event messages.
I have written/tested this with data from Wahoo+Di2, so it’s entirely possible there are nuances I missed with other products.
I also made a Chart that combines front and rear gear:
Here’s the code for one of them:
{
function cmp(a, b) {
return a.timestamp.value - b.timestamp.value;
}
let gear_event_names = ["FRONT_GEAR_CHANGE", "REAR_GEAR_CHANGE"];
let change_events = [];
let start_timestamp;
for (let m = 0; m < icu.fit.length; m++) {
let r = icu.fit[m];
if (r.event == null) continue;
if (gear_event_names.includes(r.event.valueName)) {
change_events.push(r);
} else if (r.event.valueName === "TIMER" && r.event_type.valueName === "START") {
start_timestamp = start_timestamp ? Math.min(start_timestamp, r.timestamp.value) : r.timestamp.value;
}
}
change_events.sort(cmp); // Probably unnecessary?
let value = undefined;
for (let x=0; x < icu.streams.time.length; x++) {
let t = icu.streams.time[x];
while (change_events.length > 0 && t >= change_events[0].timestamp.value - start_timestamp) {
let next_event = change_events.shift();
value = next_event.front_gear.value;
if (Array.isArray(value)) {
// I am not sure why, but sometimes the value is an array with the same value twice?
value = value[0];
}
}
data[x] = value;
}
}
The only thing different between the charts is the line starting with value = , which either get front_gear, front_gear_num, rear_gear or rear_gear_num.
I imagine this could be used as the basis for some maybe-useful charts as well. I hope someone grabs the code and makes something cool with it
I figured that was probably the case, but didn’t know how many assumptions I could make. In that case I can probably get away without collecting the events at all. I have very little experience with FIT files. I also just noticed the setAt helper, which should also save some trouble - shouldn’t need to deal with the timer start event.
After looking at documentation again, I have been able to simplify the code a fair bit. There should be no changes to the output, but it’s probably a better basis for further development.
{
const GEAR_EVENTS = ["FRONT_GEAR_CHANGE", "REAR_GEAR_CHANGE"];
let rear_gear = undefined;
let rear_gear_num = undefined;
let front_gear = undefined;
let front_gear_num = undefined;
let fit_idx = 0;
function fixValue(value) {
// I am not sure why, but sometimes the value is an array with the same value twice?
value = Array.isArray(value) ? value[0] : value;
// If value is 0, make it undefined instead
value = value === 0 ? undefined : value;
return value;
}
/*
for (let x=0; x < icu.fit.length; x++) {
let r = icu.fit[x];
if (GEAR_EVENTS.includes(r?.event?.valueName)) {
console.log("Change at", r.timestamp.value - data.startTimestamp, "Front", r.front_gear, "Rear", r.rear_gear);
}
}
*/
for (let i=0; i < data.length; i++) {
let data_time = data.time[i];
while (fit_idx < icu.fit.length) {
let msg = icu.fit[fit_idx];
// Not all messages have timestamps so this becomes NaN, which happens to work in our favour.
let msg_elapsed = msg.timestamp?.value - data.startTimestamp;
if (msg_elapsed > data_time) {
// Continue to next data point if we reach a message for a later timestamp
break;
}
if (GEAR_EVENTS.includes(msg?.event?.valueName)) {
rear_gear = fixValue(msg.rear_gear?.value);
rear_gear_num = fixValue(msg.rear_gear_num?.value);
front_gear = fixValue(msg.front_gear?.value);
front_gear_num = fixValue(msg.front_gear_num?.value);
}
fit_idx++;
}
data[i] = rear_gear;
}
}
Occasionally hitting some Memory limit exceeded errors on longer activities - not sure what I can do to help that at this point, if anything.
I made your code a little shorter. Think it still produces the same output. Could you please post a link to an activity where you get out of memory errors. That shouldn’t happen with this script.
{
let rear_gear, front_gear
function fixValue(value) {
// I am not sure why, but sometimes the value is an array with the same value twice?
value = Array.isArray(value) ? value[0] : value
return value === 0 ? null : value
}
for (let m of icu.fit) {
switch (m.event?.valueName) {
case "REAR_GEAR_CHANGE":
case "FRONT_GEAR_CHANGE":
rear_gear = fixValue(m.rear_gear?.value)
front_gear = fixValue(m.front_gear?.value)
}
let ts = m.timestamp
if (ts) data.setAt(ts.value, rear_gear)
}
}
That works nicely - I have updated the stream definitions (including gear index and ratio - only difference was also keeping front/rear_gear_num). Hadn’t thought of using the “irrelevant” fit messages as a way to set the data. Not entirely sure I understand why it might work better, but it seems to have helped.
This 12-hour activity still fails with Memory limit exceeded though: i20126210