@pond-ts/fit 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3254 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/activity/index.d.ts +254 -0
- package/dist/activity/index.js +652 -0
- package/dist/cjs-fallback.cjs +15 -0
- package/dist/geo/index.d.ts +358 -0
- package/dist/geo/index.js +864 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/intervals.d.ts +10 -0
- package/dist/intervals.js +17 -0
- package/dist/power/index.d.ts +126 -0
- package/dist/power/index.js +279 -0
- package/dist/profile/index.d.ts +125 -0
- package/dist/profile/index.js +217 -0
- package/dist/quantities.d.ts +108 -0
- package/dist/quantities.js +207 -0
- package/dist/summary/index.d.ts +138 -0
- package/dist/summary/index.js +450 -0
- package/dist/track/index.d.ts +44 -0
- package/dist/track/index.js +65 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.js +10 -0
- package/dist/units.d.ts +41 -0
- package/dist/units.js +69 -0
- package/dist/zones/index.d.ts +32 -0
- package/dist/zones/index.js +61 -0
- package/package.json +46 -0
package/dist/units.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Display-edge unit conversion. The model stores SI; UI converts here. */
|
|
2
|
+
export declare const METERS_PER_MILE = 1609.344;
|
|
3
|
+
export declare const METERS_PER_FOOT = 0.3048;
|
|
4
|
+
export declare const metersToMiles: (m: number) => number;
|
|
5
|
+
export declare const metersToFeet: (m: number) => number;
|
|
6
|
+
/** Seconds → "h:mm:ss" or "m:ss". */
|
|
7
|
+
export declare function formatDuration(totalSeconds: number): string;
|
|
8
|
+
export type DistanceUnit = 'mi' | 'km';
|
|
9
|
+
export type ElevationUnit = 'ft' | 'm';
|
|
10
|
+
export type TemperatureUnit = 'F' | 'C';
|
|
11
|
+
export type SpeedPaceUnit = 'imperial' | 'metric';
|
|
12
|
+
export interface UnitPreferences {
|
|
13
|
+
distance: DistanceUnit;
|
|
14
|
+
elevation: ElevationUnit;
|
|
15
|
+
temperature: TemperatureUnit;
|
|
16
|
+
speedPace: SpeedPaceUnit;
|
|
17
|
+
}
|
|
18
|
+
/** US defaults — the archive's origin. Each axis is independently overridable. */
|
|
19
|
+
export declare const DEFAULT_UNITS: UnitPreferences;
|
|
20
|
+
/** Distance: metres → display number in the chosen unit. */
|
|
21
|
+
export declare function convertDistance(meters: number, unit: DistanceUnit): number;
|
|
22
|
+
/** Elevation: metres → display number in the chosen unit. */
|
|
23
|
+
export declare function convertElevation(meters: number, unit: ElevationUnit): number;
|
|
24
|
+
/** Temperature: °C → display number in the chosen unit. */
|
|
25
|
+
export declare function convertTemperature(celsius: number, unit: TemperatureUnit): number;
|
|
26
|
+
/** Speed: m/s → display number (mph or km/h). */
|
|
27
|
+
export declare function convertSpeed(metersPerSecond: number, unit: SpeedPaceUnit): number;
|
|
28
|
+
/** The label shown next to a converted value, e.g. for axis ticks / stat tiles. */
|
|
29
|
+
export declare function distanceUnitLabel(unit: DistanceUnit): string;
|
|
30
|
+
export declare function elevationUnitLabel(unit: ElevationUnit): string;
|
|
31
|
+
export declare function temperatureUnitLabel(unit: TemperatureUnit): string;
|
|
32
|
+
export declare function speedUnitLabel(unit: SpeedPaceUnit): string;
|
|
33
|
+
export declare function paceUnitLabel(unit: SpeedPaceUnit): string;
|
|
34
|
+
/**
|
|
35
|
+
* Seconds-per-kilometre → "m:ss/km" (default) or "m:ss/mi" when `unit` is
|
|
36
|
+
* imperial. Rounds total seconds *first* then splits, so a pace rounding up to
|
|
37
|
+
* a full minute carries (119.88 → "2:00/km", never "1:60/km"). Same
|
|
38
|
+
* round-then-split discipline as {@link formatDuration}.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatPace(secondsPerKm: number, unit?: SpeedPaceUnit): string;
|
|
41
|
+
//# sourceMappingURL=units.d.ts.map
|
package/dist/units.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Display-edge unit conversion. The model stores SI; UI converts here. */
|
|
2
|
+
export const METERS_PER_MILE = 1609.344;
|
|
3
|
+
export const METERS_PER_FOOT = 0.3048;
|
|
4
|
+
export const metersToMiles = (m) => m / METERS_PER_MILE;
|
|
5
|
+
export const metersToFeet = (m) => m / METERS_PER_FOOT;
|
|
6
|
+
/** Seconds → "h:mm:ss" or "m:ss". */
|
|
7
|
+
export function formatDuration(totalSeconds) {
|
|
8
|
+
const s = Math.round(totalSeconds);
|
|
9
|
+
const h = Math.floor(s / 3600);
|
|
10
|
+
const m = Math.floor((s % 3600) / 60);
|
|
11
|
+
const sec = s % 60;
|
|
12
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
13
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
|
|
14
|
+
}
|
|
15
|
+
/** US defaults — the archive's origin. Each axis is independently overridable. */
|
|
16
|
+
export const DEFAULT_UNITS = {
|
|
17
|
+
distance: 'mi',
|
|
18
|
+
elevation: 'ft',
|
|
19
|
+
temperature: 'F',
|
|
20
|
+
speedPace: 'imperial',
|
|
21
|
+
};
|
|
22
|
+
/** Distance: metres → display number in the chosen unit. */
|
|
23
|
+
export function convertDistance(meters, unit) {
|
|
24
|
+
return unit === 'km' ? meters / 1000 : metersToMiles(meters);
|
|
25
|
+
}
|
|
26
|
+
/** Elevation: metres → display number in the chosen unit. */
|
|
27
|
+
export function convertElevation(meters, unit) {
|
|
28
|
+
return unit === 'm' ? meters : metersToFeet(meters);
|
|
29
|
+
}
|
|
30
|
+
/** Temperature: °C → display number in the chosen unit. */
|
|
31
|
+
export function convertTemperature(celsius, unit) {
|
|
32
|
+
return unit === 'F' ? celsius * 1.8 + 32 : celsius;
|
|
33
|
+
}
|
|
34
|
+
/** Speed: m/s → display number (mph or km/h). */
|
|
35
|
+
export function convertSpeed(metersPerSecond, unit) {
|
|
36
|
+
return unit === 'metric' ? metersPerSecond * 3.6 : metersPerSecond * 2.236936;
|
|
37
|
+
}
|
|
38
|
+
/** The label shown next to a converted value, e.g. for axis ticks / stat tiles. */
|
|
39
|
+
export function distanceUnitLabel(unit) {
|
|
40
|
+
return unit;
|
|
41
|
+
}
|
|
42
|
+
export function elevationUnitLabel(unit) {
|
|
43
|
+
return unit;
|
|
44
|
+
}
|
|
45
|
+
export function temperatureUnitLabel(unit) {
|
|
46
|
+
return unit === 'F' ? '°F' : '°C';
|
|
47
|
+
}
|
|
48
|
+
export function speedUnitLabel(unit) {
|
|
49
|
+
return unit === 'metric' ? 'km/h' : 'mph';
|
|
50
|
+
}
|
|
51
|
+
export function paceUnitLabel(unit) {
|
|
52
|
+
return unit === 'metric' ? '/km' : '/mi';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Seconds-per-kilometre → "m:ss/km" (default) or "m:ss/mi" when `unit` is
|
|
56
|
+
* imperial. Rounds total seconds *first* then splits, so a pace rounding up to
|
|
57
|
+
* a full minute carries (119.88 → "2:00/km", never "1:60/km"). Same
|
|
58
|
+
* round-then-split discipline as {@link formatDuration}.
|
|
59
|
+
*/
|
|
60
|
+
export function formatPace(secondsPerKm, unit = 'metric') {
|
|
61
|
+
const perUnit = unit === 'imperial'
|
|
62
|
+
? secondsPerKm * (METERS_PER_MILE / 1000)
|
|
63
|
+
: secondsPerKm;
|
|
64
|
+
const s = Math.round(perUnit);
|
|
65
|
+
const m = Math.floor(s / 60);
|
|
66
|
+
const sec = s % 60;
|
|
67
|
+
return `${m}:${String(sec).padStart(2, '0')}${paceUnitLabel(unit)}`;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=units.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ZoneDef } from '../profile/index.js';
|
|
2
|
+
/** One zone's time + share. `hi` is `Infinity` for the open top. */
|
|
3
|
+
export interface ZoneTime {
|
|
4
|
+
/** 1-based zone number (Z1 = the lowest band). */
|
|
5
|
+
zone: number;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Inclusive lower edge, in the value axis (watts / bpm / m·s⁻¹). */
|
|
8
|
+
lo: number;
|
|
9
|
+
/** Upper edge; `Infinity` for the top zone. */
|
|
10
|
+
hi: number;
|
|
11
|
+
seconds: number;
|
|
12
|
+
/** Share of total in-zone time, [0, 1]. */
|
|
13
|
+
fraction: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Time spent in each zone, bucketing `values` by the ascending `edges` and
|
|
17
|
+
* summing per-sample `dt`. pond `byColumn({ edges, inclusive: '(]' })` over the
|
|
18
|
+
* value axis — Coggan-style **inclusive-upper** bins natively (a sample exactly
|
|
19
|
+
* on a boundary counts in the lower zone), no ε-nudge. Non-finite values are
|
|
20
|
+
* dropped (can't be placed); sub-zero clamps to the bottom zone. pond 0.30 made
|
|
21
|
+
* the floor edge of `'(]'` inclusive (the `include_lowest` convention), so a 0
|
|
22
|
+
* sample (a stop / coast) lands in zone 1 with the edges passed as-is — no
|
|
23
|
+
* floor-push needed (F-inclusive-floor, resolved in 0.30).
|
|
24
|
+
*/
|
|
25
|
+
export declare function zoneDistributionByValue(values: ArrayLike<number>, dt: ArrayLike<number>, zones: ZoneDef): ZoneTime[];
|
|
26
|
+
/** Time in each HR zone (bpm axis). `hrZones` from `profile.profileAsOf`. */
|
|
27
|
+
export declare function hrZoneDistribution(timeSec: Float64Array, heartrate: ArrayLike<number>, hrZones: ZoneDef): ZoneTime[];
|
|
28
|
+
/** Time in each pace zone. We bucket the **speed** channel (m/s) against
|
|
29
|
+
* speed-axis edges (Z1 = slowest) so a stop doesn't blow the reciprocal up;
|
|
30
|
+
* the UI labels the bands as paces. `paceZones` from `profile.profileAsOf`. */
|
|
31
|
+
export declare function paceZoneDistribution(timeSec: Float64Array, speed: ArrayLike<number>, paceZones: ZoneDef): ZoneTime[];
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-in-zone over a **value axis** — the engine behind the power, heart-rate,
|
|
3
|
+
* and pace zone distributions. Each is "how long did this channel spend in each
|
|
4
|
+
* band," i.e. bucket the per-sample value by the zone edges and sum each
|
|
5
|
+
* sample's duration. That's pond `byColumn` over the value column, summing a
|
|
6
|
+
* gap-clamped `dt` weight — the same shape the power distribution uses,
|
|
7
|
+
* generalized so HR and pace share one tested core.
|
|
8
|
+
*/
|
|
9
|
+
import { TimeSeries } from 'pond-ts';
|
|
10
|
+
import { intervals } from '../intervals.js';
|
|
11
|
+
const BIN_SCHEMA = [
|
|
12
|
+
{ name: 'time', kind: 'time' },
|
|
13
|
+
// optional so a non-finite sample rides as `undefined` and byColumn drops it.
|
|
14
|
+
{ name: 'val', kind: 'number', required: false },
|
|
15
|
+
{ name: 'dt', kind: 'number' },
|
|
16
|
+
];
|
|
17
|
+
const SENTINEL = 1e9; // the open-top edge ZoneDef carries
|
|
18
|
+
/**
|
|
19
|
+
* Time spent in each zone, bucketing `values` by the ascending `edges` and
|
|
20
|
+
* summing per-sample `dt`. pond `byColumn({ edges, inclusive: '(]' })` over the
|
|
21
|
+
* value axis — Coggan-style **inclusive-upper** bins natively (a sample exactly
|
|
22
|
+
* on a boundary counts in the lower zone), no ε-nudge. Non-finite values are
|
|
23
|
+
* dropped (can't be placed); sub-zero clamps to the bottom zone. pond 0.30 made
|
|
24
|
+
* the floor edge of `'(]'` inclusive (the `include_lowest` convention), so a 0
|
|
25
|
+
* sample (a stop / coast) lands in zone 1 with the edges passed as-is — no
|
|
26
|
+
* floor-push needed (F-inclusive-floor, resolved in 0.30).
|
|
27
|
+
*/
|
|
28
|
+
export function zoneDistributionByValue(values, dt, zones) {
|
|
29
|
+
const { edges, labels } = zones;
|
|
30
|
+
const rows = [];
|
|
31
|
+
for (let i = 0; i < values.length; i++) {
|
|
32
|
+
const v = values[i];
|
|
33
|
+
rows.push([i, Number.isFinite(v) ? Math.max(0, v) : undefined, dt[i] ?? 0]);
|
|
34
|
+
}
|
|
35
|
+
const bins = new TimeSeries({
|
|
36
|
+
name: 'zones',
|
|
37
|
+
schema: BIN_SCHEMA,
|
|
38
|
+
rows,
|
|
39
|
+
}).byColumn('val', { edges, inclusive: '(]' }, { seconds: { from: 'dt', using: 'sum' } });
|
|
40
|
+
const secs = bins.map((b) => b.seconds ?? 0);
|
|
41
|
+
const total = secs.reduce((a, b) => a + b, 0) || 1;
|
|
42
|
+
return labels.map((label, z) => ({
|
|
43
|
+
zone: z + 1,
|
|
44
|
+
label,
|
|
45
|
+
lo: edges[z],
|
|
46
|
+
hi: (edges[z + 1] ?? SENTINEL) >= SENTINEL ? Infinity : edges[z + 1],
|
|
47
|
+
seconds: secs[z] ?? 0,
|
|
48
|
+
fraction: (secs[z] ?? 0) / total,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
/** Time in each HR zone (bpm axis). `hrZones` from `profile.profileAsOf`. */
|
|
52
|
+
export function hrZoneDistribution(timeSec, heartrate, hrZones) {
|
|
53
|
+
return zoneDistributionByValue(heartrate, intervals(timeSec), hrZones);
|
|
54
|
+
}
|
|
55
|
+
/** Time in each pace zone. We bucket the **speed** channel (m/s) against
|
|
56
|
+
* speed-axis edges (Z1 = slowest) so a stop doesn't blow the reciprocal up;
|
|
57
|
+
* the UI labels the bands as paces. `paceZones` from `profile.profileAsOf`. */
|
|
58
|
+
export function paceZoneDistribution(timeSec, speed, paceZones) {
|
|
59
|
+
return zoneDistributionByValue(speed, intervals(timeSec), paceZones);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pond-ts/fit",
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Fitness & activity domain library on pond-ts — quantities, canonical activity series, and analytics (geo, power, zones, splits)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/pjm17971/pond-ts.git",
|
|
10
|
+
"directory": "packages/fit"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/cjs-fallback.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"CHANGELOG.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
32
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
33
|
+
"prepack": "cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
|
|
34
|
+
"test": "npm run test:type && npm run test:runtime",
|
|
35
|
+
"test:type": "tsc -p tsconfig.types.json",
|
|
36
|
+
"test:runtime": "vitest run",
|
|
37
|
+
"verify": "npm run format:check && npm run build && npm test"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"pond-ts": "^0.31.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.6.3",
|
|
44
|
+
"vitest": "^2.1.4"
|
|
45
|
+
}
|
|
46
|
+
}
|