@swimedge/metrics 1.0.2 → 1.0.4
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/dist/index.d.ts +9 -4
- package/dist/index.js +82 -28
- package/dist/index.spec.js +0 -30
- package/package.json +1 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* Shared metrics registry for frontend, backend, and watch app
|
|
4
4
|
*/
|
|
5
5
|
export declare enum SwimType {
|
|
6
|
-
INDOOR = "
|
|
7
|
-
OPEN_WATER = "
|
|
6
|
+
INDOOR = "indoorPool",
|
|
7
|
+
OPEN_WATER = "openWater"
|
|
8
8
|
}
|
|
9
9
|
export interface SwimSessionLaps {
|
|
10
10
|
lapSplitTimes?: number[];
|
|
11
|
-
lapTurnTimes?: number[];
|
|
12
11
|
lapStrokeTypes?: string[];
|
|
13
12
|
lapHeartRates?: number[];
|
|
14
13
|
lapStrokeCounts?: number[];
|
|
@@ -55,6 +54,10 @@ export interface SwimSession extends SwimSessionLaps {
|
|
|
55
54
|
equipment?: string[];
|
|
56
55
|
avgDistancePerStroke?: number;
|
|
57
56
|
routeCoordinates?: number[][];
|
|
57
|
+
routeMapImage?: string;
|
|
58
|
+
deviceType?: string;
|
|
59
|
+
osVersion?: string;
|
|
60
|
+
healthKitWorkoutId?: string;
|
|
58
61
|
}
|
|
59
62
|
export interface AggregatedSwimMetrics {
|
|
60
63
|
totalDistance: number;
|
|
@@ -94,7 +97,7 @@ export declare const formatPace: (pace: string | number) => string;
|
|
|
94
97
|
export declare const formatTime: (seconds: number) => string;
|
|
95
98
|
export declare const formatHeartRate: (bpm: number) => string;
|
|
96
99
|
export declare const formatDistance: (meters: number) => string;
|
|
97
|
-
export declare const
|
|
100
|
+
export declare const formatSwimType: (swimType: SwimType | string | undefined) => string;
|
|
98
101
|
export interface MetricDefinition {
|
|
99
102
|
key: string;
|
|
100
103
|
label: string;
|
|
@@ -114,6 +117,7 @@ export interface MetricDefinition {
|
|
|
114
117
|
manual: boolean;
|
|
115
118
|
};
|
|
116
119
|
swimTypes: SwimType[];
|
|
120
|
+
hasChart?: boolean;
|
|
117
121
|
formatter?: (session: SwimSession) => string | number | null;
|
|
118
122
|
lapFormatter?: (value: any) => string;
|
|
119
123
|
condition?: (session: SwimSession) => boolean;
|
|
@@ -122,6 +126,7 @@ export declare const METRICS_REGISTRY: Record<keyof SwimSession, MetricDefinitio
|
|
|
122
126
|
export declare const MetricsHelper: {
|
|
123
127
|
getMetric: (key: keyof SwimSession) => MetricDefinition;
|
|
124
128
|
getSessionMetrics: () => MetricDefinition[];
|
|
129
|
+
getChartMetrics: () => MetricDefinition[];
|
|
125
130
|
getLapMetrics: () => MetricDefinition[];
|
|
126
131
|
getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
|
|
127
132
|
getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Shared metrics registry for frontend, backend, and watch app
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.MetricsHelper = exports.METRICS_REGISTRY = exports.
|
|
7
|
+
exports.MetricsHelper = exports.METRICS_REGISTRY = exports.formatSwimType = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
|
|
8
8
|
var SwimType;
|
|
9
9
|
(function (SwimType) {
|
|
10
|
-
SwimType["INDOOR"] = "
|
|
11
|
-
SwimType["OPEN_WATER"] = "
|
|
10
|
+
SwimType["INDOOR"] = "indoorPool";
|
|
11
|
+
SwimType["OPEN_WATER"] = "openWater";
|
|
12
12
|
})(SwimType || (exports.SwimType = SwimType = {}));
|
|
13
13
|
var MetricCategory;
|
|
14
14
|
(function (MetricCategory) {
|
|
@@ -32,7 +32,12 @@ const formatPace = (pace) => {
|
|
|
32
32
|
exports.formatPace = formatPace;
|
|
33
33
|
const formatTime = (seconds) => {
|
|
34
34
|
const mins = Math.floor(seconds / 60);
|
|
35
|
-
const secs =
|
|
35
|
+
const secs = seconds % 60;
|
|
36
|
+
if (mins >= 60) {
|
|
37
|
+
const hours = Math.floor(mins / 60);
|
|
38
|
+
const remainingMins = mins % 60;
|
|
39
|
+
return `${hours}:${remainingMins.toString().padStart(2, '0')}`;
|
|
40
|
+
}
|
|
36
41
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
37
42
|
};
|
|
38
43
|
exports.formatTime = formatTime;
|
|
@@ -46,16 +51,15 @@ const formatDistance = (meters) => {
|
|
|
46
51
|
: `${Math.round(meters)}m`;
|
|
47
52
|
};
|
|
48
53
|
exports.formatDistance = formatDistance;
|
|
49
|
-
const
|
|
50
|
-
if (!
|
|
51
|
-
return '';
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return `${(0, exports.formatNumber)(avgTime, 1)}s`;
|
|
54
|
+
const formatSwimType = (swimType) => {
|
|
55
|
+
if (!swimType)
|
|
56
|
+
return 'Unknown';
|
|
57
|
+
return ({
|
|
58
|
+
[SwimType.INDOOR]: 'Indoor Pool',
|
|
59
|
+
[SwimType.OPEN_WATER]: 'Open Water',
|
|
60
|
+
}[swimType] || swimType);
|
|
57
61
|
};
|
|
58
|
-
exports.
|
|
62
|
+
exports.formatSwimType = formatSwimType;
|
|
59
63
|
exports.METRICS_REGISTRY = {
|
|
60
64
|
distance: {
|
|
61
65
|
key: 'distance',
|
|
@@ -70,6 +74,7 @@ exports.METRICS_REGISTRY = {
|
|
|
70
74
|
isHealthKit: true,
|
|
71
75
|
availableFrom: { watch: true, manual: true },
|
|
72
76
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
77
|
+
hasChart: true,
|
|
73
78
|
formatter: (s) => (0, exports.formatDistance)(s.distance),
|
|
74
79
|
},
|
|
75
80
|
time: {
|
|
@@ -85,6 +90,7 @@ exports.METRICS_REGISTRY = {
|
|
|
85
90
|
isHealthKit: true,
|
|
86
91
|
availableFrom: { watch: true, manual: true },
|
|
87
92
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
93
|
+
hasChart: true,
|
|
88
94
|
formatter: (s) => (0, exports.formatTime)(s.time),
|
|
89
95
|
},
|
|
90
96
|
activeTime: {
|
|
@@ -99,6 +105,7 @@ exports.METRICS_REGISTRY = {
|
|
|
99
105
|
healthKitSource: 'HKWorkoutBuilder.elapsedTime',
|
|
100
106
|
isHealthKit: true,
|
|
101
107
|
availableFrom: { watch: true, manual: false },
|
|
108
|
+
hasChart: true,
|
|
102
109
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
103
110
|
},
|
|
104
111
|
restTime: {
|
|
@@ -112,6 +119,7 @@ exports.METRICS_REGISTRY = {
|
|
|
112
119
|
formula: 'Total Duration - Active Time',
|
|
113
120
|
availableFrom: { watch: true, manual: false },
|
|
114
121
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
122
|
+
hasChart: true,
|
|
115
123
|
formatter: (s) => (0, exports.formatTime)(s.restTime),
|
|
116
124
|
condition: (s) => !!s.restTime,
|
|
117
125
|
},
|
|
@@ -126,6 +134,7 @@ exports.METRICS_REGISTRY = {
|
|
|
126
134
|
formula: 'Distance ÷ Pool Length',
|
|
127
135
|
availableFrom: { watch: true, manual: true },
|
|
128
136
|
swimTypes: [SwimType.INDOOR],
|
|
137
|
+
hasChart: true,
|
|
129
138
|
formatter: (s) => String(s.laps),
|
|
130
139
|
condition: (s) => !!s.laps,
|
|
131
140
|
},
|
|
@@ -140,6 +149,7 @@ exports.METRICS_REGISTRY = {
|
|
|
140
149
|
formula: '(Active Time ÷ Distance) × 100',
|
|
141
150
|
availableFrom: { watch: true, manual: true },
|
|
142
151
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
152
|
+
hasChart: true,
|
|
143
153
|
formatter: (s) => (0, exports.formatPace)(s.avgPace),
|
|
144
154
|
condition: (s) => !!s.avgPace,
|
|
145
155
|
},
|
|
@@ -168,7 +178,7 @@ exports.METRICS_REGISTRY = {
|
|
|
168
178
|
source: 'User selection or workout location type',
|
|
169
179
|
availableFrom: { watch: true, manual: true },
|
|
170
180
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
171
|
-
formatter: (s) => s.swimType,
|
|
181
|
+
formatter: (s) => (0, exports.formatSwimType)(s.swimType),
|
|
172
182
|
condition: (s) => !!s.swimType,
|
|
173
183
|
},
|
|
174
184
|
waterTemp: {
|
|
@@ -247,6 +257,7 @@ exports.METRICS_REGISTRY = {
|
|
|
247
257
|
isHealthKit: true,
|
|
248
258
|
availableFrom: { watch: true, manual: true },
|
|
249
259
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
260
|
+
hasChart: true,
|
|
250
261
|
formatter: (s) => String(s.calories),
|
|
251
262
|
condition: (s) => !!s.calories,
|
|
252
263
|
},
|
|
@@ -263,6 +274,7 @@ exports.METRICS_REGISTRY = {
|
|
|
263
274
|
isHealthKit: true,
|
|
264
275
|
availableFrom: { watch: true, manual: false },
|
|
265
276
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
277
|
+
hasChart: true,
|
|
266
278
|
formatter: (s) => String(s.totalStrokeCount || 0),
|
|
267
279
|
condition: (s) => !!s.totalStrokeCount,
|
|
268
280
|
},
|
|
@@ -277,6 +289,7 @@ exports.METRICS_REGISTRY = {
|
|
|
277
289
|
formula: 'Total Strokes ÷ (Active Time in Minutes)',
|
|
278
290
|
availableFrom: { watch: true, manual: true },
|
|
279
291
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
292
|
+
hasChart: true,
|
|
280
293
|
formatter: (s) => `${(0, exports.formatNumber)(s.avgStrokeRate, 1)} SPM`,
|
|
281
294
|
condition: (s) => !!s.avgStrokeRate,
|
|
282
295
|
},
|
|
@@ -291,6 +304,7 @@ exports.METRICS_REGISTRY = {
|
|
|
291
304
|
formula: 'Distance ÷ Total Strokes',
|
|
292
305
|
availableFrom: { watch: true, manual: true },
|
|
293
306
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
307
|
+
hasChart: true,
|
|
294
308
|
formatter: (s) => `${(0, exports.formatNumber)(s.avgDistancePerStroke, 2)}m`,
|
|
295
309
|
condition: (s) => !!s.avgDistancePerStroke,
|
|
296
310
|
},
|
|
@@ -305,6 +319,7 @@ exports.METRICS_REGISTRY = {
|
|
|
305
319
|
formula: 'Average of (Lap Time in Seconds + Lap Stroke Count) for each lap',
|
|
306
320
|
availableFrom: { watch: true, manual: true },
|
|
307
321
|
swimTypes: [SwimType.INDOOR],
|
|
322
|
+
hasChart: true,
|
|
308
323
|
formatter: (s) => (0, exports.formatNumber)(s.avgSwolfScore, 0),
|
|
309
324
|
condition: (s) => !!s.avgSwolfScore,
|
|
310
325
|
},
|
|
@@ -405,20 +420,21 @@ exports.METRICS_REGISTRY = {
|
|
|
405
420
|
availableFrom: { watch: false, manual: false },
|
|
406
421
|
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
407
422
|
},
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
// HealthKit doesn't track turn times
|
|
424
|
+
/*lapTurnTimes: {
|
|
425
|
+
key: 'lapTurnTimes',
|
|
426
|
+
label: 'Turn',
|
|
427
|
+
unit: 's',
|
|
428
|
+
category: MetricCategory.TECHNIQUE,
|
|
429
|
+
description: 'Time spent turning at pool wall per lap',
|
|
430
|
+
icon: 'refresh-circle-outline',
|
|
431
|
+
isPerLap: true,
|
|
432
|
+
decimals: 2,
|
|
433
|
+
availableFrom: { watch: true, manual: false },
|
|
434
|
+
swimTypes: [SwimType.INDOOR],
|
|
435
|
+
formatter: formatAverageTurnTime,
|
|
436
|
+
condition: (s) => !!s.lapTurnTimes?.length,
|
|
437
|
+
},*/
|
|
422
438
|
lapStrokeTypes: {
|
|
423
439
|
key: 'lapStrokeTypes',
|
|
424
440
|
label: 'Stroke',
|
|
@@ -565,10 +581,48 @@ exports.METRICS_REGISTRY = {
|
|
|
565
581
|
formatter: (s) => s.routeCoordinates ? `${s.routeCoordinates.length} points` : '',
|
|
566
582
|
condition: (s) => !!s.routeCoordinates?.length,
|
|
567
583
|
},
|
|
584
|
+
deviceType: {
|
|
585
|
+
key: 'deviceType',
|
|
586
|
+
label: 'Device',
|
|
587
|
+
category: MetricCategory.PERFORMANCE,
|
|
588
|
+
description: 'Device used to record the session',
|
|
589
|
+
isPerSession: true,
|
|
590
|
+
availableFrom: { watch: true, manual: false },
|
|
591
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
592
|
+
},
|
|
593
|
+
osVersion: {
|
|
594
|
+
key: 'osVersion',
|
|
595
|
+
label: 'OS Version',
|
|
596
|
+
category: MetricCategory.PERFORMANCE,
|
|
597
|
+
description: 'Operating system version of the recording device',
|
|
598
|
+
isPerSession: true,
|
|
599
|
+
availableFrom: { watch: true, manual: false },
|
|
600
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
601
|
+
},
|
|
602
|
+
healthKitWorkoutId: {
|
|
603
|
+
key: 'healthKitWorkoutId',
|
|
604
|
+
label: 'HealthKit ID',
|
|
605
|
+
category: MetricCategory.PERFORMANCE,
|
|
606
|
+
description: 'HealthKit workout identifier',
|
|
607
|
+
isPerSession: true,
|
|
608
|
+
availableFrom: { watch: true, manual: false },
|
|
609
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
610
|
+
},
|
|
611
|
+
routeMapImage: {
|
|
612
|
+
key: 'routeMapImage',
|
|
613
|
+
label: 'Route Map',
|
|
614
|
+
category: MetricCategory.PERFORMANCE,
|
|
615
|
+
description: 'Base64 encoded route map image for open water sessions',
|
|
616
|
+
isPerSession: true,
|
|
617
|
+
availableFrom: { watch: false, manual: false },
|
|
618
|
+
swimTypes: [SwimType.OPEN_WATER],
|
|
619
|
+
condition: (s) => !!s.routeMapImage,
|
|
620
|
+
},
|
|
568
621
|
};
|
|
569
622
|
exports.MetricsHelper = {
|
|
570
623
|
getMetric: (key) => exports.METRICS_REGISTRY[key],
|
|
571
624
|
getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
|
|
625
|
+
getChartMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession && m.hasChart),
|
|
572
626
|
getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
|
|
573
627
|
getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
|
|
574
628
|
getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(swimType)),
|
package/dist/index.spec.js
CHANGED
|
@@ -72,34 +72,4 @@ describe('Metrics Formatting Functions', () => {
|
|
|
72
72
|
expect((0, index_1.formatDistance)(500.7)).toBe('501m');
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
-
describe('formatAverageTurnTime', () => {
|
|
76
|
-
const createSession = (lapTurnTimes) => ({
|
|
77
|
-
id: '1',
|
|
78
|
-
date: '2024-01-01',
|
|
79
|
-
distance: 1000,
|
|
80
|
-
laps: 20,
|
|
81
|
-
time: 1200,
|
|
82
|
-
calories: 300,
|
|
83
|
-
avgPace: '2:00',
|
|
84
|
-
poolLength: 50,
|
|
85
|
-
source: 'watch',
|
|
86
|
-
lapTurnTimes,
|
|
87
|
-
});
|
|
88
|
-
it('should return empty string when no lap turn times', () => {
|
|
89
|
-
expect((0, index_1.formatAverageTurnTime)(createSession())).toBe('');
|
|
90
|
-
expect((0, index_1.formatAverageTurnTime)(createSession([]))).toBe('');
|
|
91
|
-
});
|
|
92
|
-
it('should calculate average turn time', () => {
|
|
93
|
-
expect((0, index_1.formatAverageTurnTime)(createSession([2.5, 3.0, 2.5]))).toBe('2.7s');
|
|
94
|
-
});
|
|
95
|
-
it('should filter out invalid turn times', () => {
|
|
96
|
-
expect((0, index_1.formatAverageTurnTime)(createSession([2.5, NaN, 3.0, 0, 2.5]))).toBe('2.7s');
|
|
97
|
-
});
|
|
98
|
-
it('should return empty string when all turn times are invalid', () => {
|
|
99
|
-
expect((0, index_1.formatAverageTurnTime)(createSession([NaN, 0, -1]))).toBe('');
|
|
100
|
-
});
|
|
101
|
-
it('should format with 1 decimal place', () => {
|
|
102
|
-
expect((0, index_1.formatAverageTurnTime)(createSession([2.456]))).toBe('2.5s');
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
75
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swimedge/metrics",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"prepublishOnly": "npm run build",
|
|
17
16
|
"build": "tsc",
|
|
18
17
|
"watch": "tsc --watch",
|
|
19
18
|
"export-json": "node scripts/export-json.js",
|