@0x3639/intervals-icu
    Preparing search index...

    Analytics

    client.analytics computes per-activity histograms (power, heart rate, pace, GAP), time-at-heart-rate, interval statistics and power models, plus an athlete-level mean-maximal power (MMP) model used to resolve %MMP workout steps. Activity ids are URL-encoded before use; the older single-curve activity methods remain on ActivityService.

    Method What it does
    AnalyticsService.getPowerHistogram Time in power buckets
    AnalyticsService.getHRHistogram Time in heart-rate buckets
    AnalyticsService.getPaceHistogram Time in pace buckets
    AnalyticsService.getGAPHistogram Time in grade-adjusted-pace buckets
    AnalyticsService.getTimeAtHR Seconds spent at each heart rate
    AnalyticsService.getIntervalStats Statistics for an arbitrary index range of the activity, as if it were an interval
    AnalyticsService.getPowerSpikeModel Power model fitted to the activity, used to detect power spikes
    AnalyticsService.getCurves Curves (watts, pace, HR, etc.) for one activity, optionally with fatigued variants
    AnalyticsService.getCurvesCSV Same as getCurves, as CSV
    AnalyticsService.getMMPModel Power model used to resolve %MMP workout steps for a sport type

    For the newest typed activity in the last year, fetch its power histogram, HR histogram and time-at-heart-rate plot. The API returns HTTP 422 when a requested stream is not available for the activity; each call is wrapped separately so one missing stream does not stop the others.

    const client = new IntervalsClient({ apiKey });

    const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    const now = new Date(); // one timestamp for both bounds
    const yearAgo = new Date(now);
    yearAgo.setFullYear(yearAgo.getFullYear() - 1);
    const newest = localDate(now);
    const oldest = localDate(yearAgo);

    const activities = await client.activities.listActivities({ oldest, newest });
    const activity = activities.find((a) => a.type);

    if (!activity) {
    console.log('No activities with a type in the last year');
    } else {
    const id = activity.id!;

    try {
    const powerBuckets = await client.analytics.getPowerHistogram(id, { bucketSize: 50 });
    console.log(`Power histogram: ${powerBuckets.length} bucket(s)`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`Power histogram not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }

    try {
    const hrBuckets = await client.analytics.getHRHistogram(id);
    console.log(`HR histogram: ${hrBuckets.length} bucket(s)`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`HR histogram not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }

    try {
    const timeAtHR = await client.analytics.getTimeAtHR(id);
    console.log(`Time at HR: ${timeAtHR.secs?.length ?? 0} bpm bucket(s)`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`Time at HR not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }
    }

    Fetch the athlete's ride power model, then for the newest typed activity in the last year: its power spike model, its watts curve (normal fatigue only), and interval stats for its first detected interval, if any. AnalyticsService.getMMPModel field names ( PowerModel) follow the spec's PowerModel schema.

    const client = new IntervalsClient({ apiKey });

    const mmpModel = await client.analytics.getMMPModel('Ride');
    console.log(`MMP model: type=${mmpModel.type ?? 'n/a'} criticalPower=${mmpModel.criticalPower ?? 'n/a'} wPrime=${mmpModel.wPrime ?? 'n/a'} pMax=${mmpModel.pMax ?? 'n/a'}`);

    const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    const now = new Date(); // one timestamp for both bounds
    const yearAgo = new Date(now);
    yearAgo.setFullYear(yearAgo.getFullYear() - 1);
    const newest = localDate(now);
    const oldest = localDate(yearAgo);

    const activities = await client.activities.listActivities({ oldest, newest });
    const activity = activities.find((a) => a.type);

    if (!activity) {
    console.log('No activities with a type in the last year');
    } else {
    const id = activity.id!;

    try {
    const spikeModel = await client.analytics.getPowerSpikeModel(id);
    console.log(`Power spike model: type=${spikeModel.type ?? 'n/a'} criticalPower=${spikeModel.criticalPower ?? 'n/a'}`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`Power spike model not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }

    try {
    const curves = await client.analytics.getCurves(id, { types: ['watts'], fatigue: ['normal'] });
    console.log(`Curves: ${curves.length}`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`Curves not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }

    const intervals = await client.activities.getIntervals(id);
    const firstInterval = intervals.icu_intervals?.[0];
    if (!firstInterval || firstInterval.start_index === undefined || firstInterval.end_index === undefined) {
    console.log('No detected intervals with an index range');
    } else {
    try {
    const stats = await client.analytics.getIntervalStats(id, firstInterval.start_index, firstInterval.end_index);
    console.log(`Interval stats: average_watts=${stats.average_watts ?? 'n/a'} average_heartrate=${stats.average_heartrate ?? 'n/a'}`);
    } catch (err) {
    if (err instanceof IntervalsAPIError && err.status === 422) {
    console.log(`Interval stats not available for ${id}: status=${err.status}`);
    } else {
    throw err;
    }
    }
    }
    }
    • AnalyticsService.getCurves returns HTTP 422 for streams or fatigue levels the activity does not have; only types: ['watts'] with fatigue: ['normal'] is confirmed to succeed on the live test account.
    • PowerModel field names follow the vendored spec.
    • See API behaviour for the cross-service list.