Totals - kJs in Power Zones

Hi,

I do not want to annoy everyone with the full logic behind this suggested feature but sometimes the time in power zone does not give a fully complete picture of effort dedicated to each exertion level.

A histogram based in kilojoules spent can be much more enlightening specially in narrow zones like SS.

The calculation can be simplified using just the midpoint of the zone in W, multiplied by seconds spend and divided by 1000.

However, it can be much more precise if it takes into account a full watt by watt time distribution aggregated by zone afterwards.

Another variation could be accruing the exertion per zone using cumulative load in zone (TSS) as the measured variable.

4 Likes

Thats interesting. I have added it to the todo list. Tx.

1 Like

Yes, being able to visualize kj per year/season/any other time frame in the Totals section would indeed be great to have. I know it’s possible to add a graph with the total kj per period, but a bar chart in the total section would be even better. Cheers!

2 Likes

This is a very nice feature to have, now specially more useful with the custom zones. Do you have a timeline for this feature?

I’ve created custom fields KJ1 - KJ2 - KJ3 etc… that you can add to your activity and then build your own chart

1 Like

How do I see it?

You have to search them in the custom fields box. I set them to be seen by everyone

Is this what you are looking for?

// --- Configuration ---
const colors = [
  'rgb(173, 216, 230)', // Z1 - Light Blue
  'rgb(0, 128, 0)',     // Z2 - Green
  'rgb(255, 215, 0)',   // Z3 - Yellow
  'rgb(255, 140, 0)',   // Z4 - Orange
  'rgb(255, 0, 0)',     // Z5 - Red
  'rgb(128, 0, 128)',   // Z6 - Purple
  'rgb(0, 0, 0)',       // Z7 - Black
  'rgb(169, 169, 169)', // Unclassified - Gray
  'rgb(0, 191, 255)'    // Sweet Spot - Deep Sky Blue
];

// --- Data Extraction ---
const act = icu.activity;
const ftp = act.icu_ftp;
const zones = act.icu_power_zones; // Expected: [55, 75, 90, 105, 120, 150]
const powerStream = icu.streams.get("fixed_watts")?.data || [];

// --- Zone Definitions ---
const zoneDefs = [
  { name: 'Z1', lower: 0.0, upper: zones[0] / 100 },
  { name: 'Z2', lower: zones[0] / 100, upper: zones[1] / 100 },
  { name: 'Z3', lower: zones[1] / 100, upper: zones[2] / 100 },
  { name: 'Z4', lower: zones[2] / 100, upper: zones[3] / 100 },
  { name: 'Z5', lower: zones[3] / 100, upper: zones[4] / 100 },
  { name: 'Z6', lower: zones[4] / 100, upper: zones[5] / 100 },
  { name: 'Z7', lower: zones[5] / 100, upper: Infinity }
];

const bounds = zoneDefs.map(z => ({
  name: z.name,
  lower: ftp * z.lower,
  upper: ftp * z.upper
}));

// --- Energy in Zones ---
let joules = new Array(8).fill(0); // 7 zones + 1 unclassified
powerStream.forEach(w => {
  if (w == null) return;
  let classified = false;
  for (let i = 0; i < 7; i++) {
    if (w >= bounds[i].lower && w < bounds[i].upper) {
      joules[i] += w;
      classified = true;
      break;
    }
  }
  if (!classified) {
    joules[7] += w; // Unclassified
  }
});

let kJ = joules.map(j => j / 1000);
let trueTotalKJ = powerStream.reduce((sum, w) => sum + (w || 0), 0) / 1000;
let perc = kJ.map(j => trueTotalKJ > 0 ? (j / trueTotalKJ) * 100 : 0);

// --- Sweet Spot Zone ---
const sweetSpotLower = ftp * 0.84;
const sweetSpotUpper = ftp * 0.97;
let sweetSpotJoules = 0;

powerStream.forEach(w => {
  if (w == null) return;
  if (w >= sweetSpotLower && w < sweetSpotUpper) {
    sweetSpotJoules += w;
  }
});

const sweetSpotKJ = sweetSpotJoules / 1000;
const sweetSpotPerc = trueTotalKJ > 0 ? (sweetSpotKJ / trueTotalKJ) * 100 : 0;

// --- Plotly Traces ---
let traces = [];
for (let i = 0; i < 8; i++) {
  let label = i < 7 ? `Zone ${i + 1}` : "Unclassified";
  let lower = i < 7 ? Math.round(bounds[i].lower) : "<" + Math.round(bounds[0].lower);
  let upper = i < 7 ? (bounds[i].upper === Infinity ? "∞" : Math.round(bounds[i].upper)) : "";
  traces.push({
    x: [i + 1],
    y: [kJ[i]],
    orientation: 'v',
    name: label,
    type: 'bar',
    marker: { color: colors[i] },
    hovertemplate: `<b>${label}</b><br>${lower}${upper ? "–" + upper : ""} watts<br>${perc[i].toFixed(1)}% of total energy<br>${kJ[i].toFixed(1)} kJ<extra></extra>`
  });
}

// --- Sweet Spot Trace ---
traces.push({
  x: [9],
  y: [sweetSpotKJ],
  orientation: 'v',
  name: 'Sweet Spot',
  type: 'bar',
  marker: { color: colors[8] },
  hovertemplate: `<b>Sweet Spot Zone</b><br>${Math.round(sweetSpotLower)}–${Math.round(sweetSpotUpper)} watts<br>${sweetSpotPerc.toFixed(1)}% of total energy<br>${sweetSpotKJ.toFixed(1)} kJ<extra></extra>`
});

// --- Layout ---
let powerZoneLayout = {
  height: 300,
  title: {
    text: `Energy Spent in Power Zones<br><sup>FTP: ${ftp} watts — Total: ${trueTotalKJ.toFixed(1)} kJ</sup>`,
    xref: 'paper',
    x: 0.5
  },
  margin: { b: 50, l: 50, r: 30, t: 90 },
  barmode: 'stack',
  hoverlabel: {
    align: 'left',
    bgcolor: 'white',
    font: { color: 'black' }
  },
  hovermode: 'closest',
  showlegend: false,
  yaxis: {
    title: "Energy (kJ)",
    showgrid: true,
    zeroline: true
  },
  xaxis: {
    zeroline: false,
    showgrid: false,
    tickvals: [1, 2, 3, 4, 5, 6, 7, 8, 9],
    ticktext: ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'Z7+', 'Unclassified', 'Sweet Spot']
  }
};

// --- Final Chart ---
let chart = { data: traces, layout: powerZoneLayout };
chart;
1 Like