Skip to main content

@pond-ts/fit

Read your own activity files, look at any part of them, and work out what they mean for the athlete who did them. @pond-ts/fit is the fitness layer on pond-ts: FIT, GPX and TCX files in; distances, times, paces, heart rate, power, laps, splits, best efforts and training load out, with every number in a type that knows its unit.

An activity records what happened. Sections let you explore any part of it. Analysis explains what it means for the athlete.

npm install pond-ts @pond-ts/fit

A first look​

Every example on these pages reads one of three made-up activities. Download them to follow along: hill-ride.fit (a 46 km ride with a power meter), interval-run.fit (six 800 m repeats) and pool-swim.fit (a set in a 25 m pool). They are modelled, not anyone's real recording; the guides' introduction says how they were made.

import { readFileSync } from 'node:fs';
import { km, minutes } from '@pond-ts/fit';
import { readActivity } from '@pond-ts/fit/files';

const ride = readActivity(readFileSync('hill-ride.fit'), {
name: 'Two climbs loop',
});

const stats = ride.stats();
console.log(ride.name, '·', ride.sport, '·', ride.startedAt.toISOString());
console.log(stats.distance?.format('km', 1), stats.time.elapsed?.format());
console.log('moving', stats.time.moving?.format());
console.log('heart rate', stats.heartRate.average?.format());
console.log(
'power',
stats.power.average?.format(),
'normalized',
stats.power.normalized?.format(),
);
console.log('climbing', stats.elevation.gain?.format('m'));
Two climbs loop · ride · 2026-06-14T07:30:00.000Z
45.0 km 1:39:54
moving 1:33:11
heart rate 133 bpm
power 190 W normalized 228 W
climbing 597 m

The same questions work on any stretch of the ride: a lap, a split, or a window you pick by time or by distance.

const climb = ride.section({ distance: { from: km(28), to: km(32) } });
console.log('second climb', climb.stats().power.average?.format());

const second5k = ride.splits(km(5)).at(1)!;
console.log(second5k.label, second5k.stats().time.elapsed?.format());

const lap = ride.laps.at(2)!;
console.log(lap.label, lap.stats().distance?.format('km', 1));
second climb 303 W
Split 2 11:50
Lap 3 10.0 km

Anything that depends on the athlete (their FTP, their heart-rate zones) goes through analyze, and anything the settings can't answer comes back null rather than as a made-up number.

const athlete = { ftpWatts: [{ at: '2026-01-01', value: 250 }] };
const analysis = ride.analyze({ athlete });
console.log('IF', analysis.power.intensityFactor?.toFixed(2));
console.log('TSS', analysis.load?.value.toFixed(0));
console.log('best 20 min', ride.power.bestEffort(minutes(20))?.power.format());
IF 0.91
TSS 139
best 20 min 269 W

The four objects​

ObjectWhat it is
ActivityThe whole recording, with its name, sport, start and device
SectionOne continuous stretch of an activity
SectionsA collection of stretches: the laps, the splits, a lap group
AnalysisResults that need the athlete's settings (FTP, zones, weight)

An Activity is itself a Section (the stretch from start to finish), so everything a section answers, the whole activity answers too. Properties say what the recording holds (name, sport, laps); methods select, calculate or transform (stats(), section(), splits(), analyze()).

Rules that hold everywhere​

  • Missing is null; a real zero stays zero. A ride with no power meter has stats.power.average === null, not 0 W. A second spent coasting is a real 0 W.
  • Time is never ambiguous. Elapsed, timer, moving and swimming time are separate clocks, and every stats() result says which one it used. See Clocks.
  • Numbers carry units. A Distance formats as km or miles, a Pace per km or per mile, and nothing takes a bare number of metres where it could be misread. See Quantities.
  • Gaps stay gaps. A dropout in the heart-rate strap or the power meter is missing data, not something to draw a line through.

Where to go next​