@swimedge/metrics 1.0.1 → 1.0.3
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 +13 -4
- package/dist/index.js +134 -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[];
|
|
@@ -54,6 +53,11 @@ export interface SwimSession extends SwimSessionLaps {
|
|
|
54
53
|
perceivedEffort?: string;
|
|
55
54
|
equipment?: string[];
|
|
56
55
|
avgDistancePerStroke?: number;
|
|
56
|
+
routeCoordinates?: number[][];
|
|
57
|
+
routeMapImage?: string;
|
|
58
|
+
deviceType?: string;
|
|
59
|
+
osVersion?: string;
|
|
60
|
+
healthKitWorkoutId?: string;
|
|
57
61
|
}
|
|
58
62
|
export interface AggregatedSwimMetrics {
|
|
59
63
|
totalDistance: number;
|
|
@@ -93,7 +97,7 @@ export declare const formatPace: (pace: string | number) => string;
|
|
|
93
97
|
export declare const formatTime: (seconds: number) => string;
|
|
94
98
|
export declare const formatHeartRate: (bpm: number) => string;
|
|
95
99
|
export declare const formatDistance: (meters: number) => string;
|
|
96
|
-
export declare const
|
|
100
|
+
export declare const formatSwimType: (swimType: SwimType | string | undefined) => string;
|
|
97
101
|
export interface MetricDefinition {
|
|
98
102
|
key: string;
|
|
99
103
|
label: string;
|
|
@@ -112,6 +116,8 @@ export interface MetricDefinition {
|
|
|
112
116
|
watch: boolean;
|
|
113
117
|
manual: boolean;
|
|
114
118
|
};
|
|
119
|
+
swimTypes: SwimType[];
|
|
120
|
+
hasChart?: boolean;
|
|
115
121
|
formatter?: (session: SwimSession) => string | number | null;
|
|
116
122
|
lapFormatter?: (value: any) => string;
|
|
117
123
|
condition?: (session: SwimSession) => boolean;
|
|
@@ -122,5 +128,8 @@ export declare const MetricsHelper: {
|
|
|
122
128
|
getSessionMetrics: () => MetricDefinition[];
|
|
123
129
|
getLapMetrics: () => MetricDefinition[];
|
|
124
130
|
getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
|
|
131
|
+
getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
|
|
132
|
+
getIndoorMetrics: () => MetricDefinition[];
|
|
133
|
+
getOpenWaterMetrics: () => MetricDefinition[];
|
|
125
134
|
getAllMetricKeys: () => string[];
|
|
126
135
|
};
|
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',
|
|
@@ -69,6 +73,8 @@ exports.METRICS_REGISTRY = {
|
|
|
69
73
|
healthKitSource: 'HKWorkout.totalDistance',
|
|
70
74
|
isHealthKit: true,
|
|
71
75
|
availableFrom: { watch: true, manual: true },
|
|
76
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
77
|
+
hasChart: true,
|
|
72
78
|
formatter: (s) => (0, exports.formatDistance)(s.distance),
|
|
73
79
|
},
|
|
74
80
|
time: {
|
|
@@ -83,6 +89,8 @@ exports.METRICS_REGISTRY = {
|
|
|
83
89
|
healthKitSource: 'HKWorkout.duration',
|
|
84
90
|
isHealthKit: true,
|
|
85
91
|
availableFrom: { watch: true, manual: true },
|
|
92
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
93
|
+
hasChart: true,
|
|
86
94
|
formatter: (s) => (0, exports.formatTime)(s.time),
|
|
87
95
|
},
|
|
88
96
|
activeTime: {
|
|
@@ -97,6 +105,8 @@ exports.METRICS_REGISTRY = {
|
|
|
97
105
|
healthKitSource: 'HKWorkoutBuilder.elapsedTime',
|
|
98
106
|
isHealthKit: true,
|
|
99
107
|
availableFrom: { watch: true, manual: false },
|
|
108
|
+
hasChart: true,
|
|
109
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
100
110
|
},
|
|
101
111
|
restTime: {
|
|
102
112
|
key: 'restTime',
|
|
@@ -108,6 +118,8 @@ exports.METRICS_REGISTRY = {
|
|
|
108
118
|
isPerSession: true,
|
|
109
119
|
formula: 'Total Duration - Active Time',
|
|
110
120
|
availableFrom: { watch: true, manual: false },
|
|
121
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
122
|
+
hasChart: true,
|
|
111
123
|
formatter: (s) => (0, exports.formatTime)(s.restTime),
|
|
112
124
|
condition: (s) => !!s.restTime,
|
|
113
125
|
},
|
|
@@ -121,6 +133,8 @@ exports.METRICS_REGISTRY = {
|
|
|
121
133
|
isPerSession: true,
|
|
122
134
|
formula: 'Distance ÷ Pool Length',
|
|
123
135
|
availableFrom: { watch: true, manual: true },
|
|
136
|
+
swimTypes: [SwimType.INDOOR],
|
|
137
|
+
hasChart: true,
|
|
124
138
|
formatter: (s) => String(s.laps),
|
|
125
139
|
condition: (s) => !!s.laps,
|
|
126
140
|
},
|
|
@@ -134,6 +148,8 @@ exports.METRICS_REGISTRY = {
|
|
|
134
148
|
isPerSession: true,
|
|
135
149
|
formula: '(Active Time ÷ Distance) × 100',
|
|
136
150
|
availableFrom: { watch: true, manual: true },
|
|
151
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
152
|
+
hasChart: true,
|
|
137
153
|
formatter: (s) => (0, exports.formatPace)(s.avgPace),
|
|
138
154
|
condition: (s) => !!s.avgPace,
|
|
139
155
|
},
|
|
@@ -147,6 +163,7 @@ exports.METRICS_REGISTRY = {
|
|
|
147
163
|
isPerSession: true,
|
|
148
164
|
source: 'User configuration setting',
|
|
149
165
|
availableFrom: { watch: true, manual: true },
|
|
166
|
+
swimTypes: [SwimType.INDOOR],
|
|
150
167
|
formatter: (s) => `${s.poolLength}m`,
|
|
151
168
|
condition: (s) => !!s.poolLength,
|
|
152
169
|
},
|
|
@@ -160,7 +177,8 @@ exports.METRICS_REGISTRY = {
|
|
|
160
177
|
isPerSession: true,
|
|
161
178
|
source: 'User selection or workout location type',
|
|
162
179
|
availableFrom: { watch: true, manual: true },
|
|
163
|
-
|
|
180
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
181
|
+
formatter: (s) => (0, exports.formatSwimType)(s.swimType),
|
|
164
182
|
condition: (s) => !!s.swimType,
|
|
165
183
|
},
|
|
166
184
|
waterTemp: {
|
|
@@ -174,6 +192,7 @@ exports.METRICS_REGISTRY = {
|
|
|
174
192
|
source: 'Apple Watch water temperature sensor',
|
|
175
193
|
isHealthKit: true,
|
|
176
194
|
availableFrom: { watch: true, manual: false },
|
|
195
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
177
196
|
formatter: (s) => `${(0, exports.formatNumber)(s.waterTemp, 1)}°C`,
|
|
178
197
|
condition: (s) => !!s.waterTemp,
|
|
179
198
|
},
|
|
@@ -189,6 +208,7 @@ exports.METRICS_REGISTRY = {
|
|
|
189
208
|
healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
|
|
190
209
|
isHealthKit: true,
|
|
191
210
|
availableFrom: { watch: true, manual: false },
|
|
211
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
192
212
|
formatter: (s) => (0, exports.formatHeartRate)(s.avgHeartRate),
|
|
193
213
|
condition: (s) => !!s.avgHeartRate,
|
|
194
214
|
},
|
|
@@ -204,6 +224,7 @@ exports.METRICS_REGISTRY = {
|
|
|
204
224
|
healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
|
|
205
225
|
isHealthKit: true,
|
|
206
226
|
availableFrom: { watch: true, manual: false },
|
|
227
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
207
228
|
formatter: (s) => (0, exports.formatHeartRate)(s.maxHeartRate),
|
|
208
229
|
condition: (s) => !!s.maxHeartRate,
|
|
209
230
|
},
|
|
@@ -219,6 +240,7 @@ exports.METRICS_REGISTRY = {
|
|
|
219
240
|
healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
|
|
220
241
|
isHealthKit: true,
|
|
221
242
|
availableFrom: { watch: true, manual: false },
|
|
243
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
222
244
|
formatter: (s) => (0, exports.formatHeartRate)(s.minHeartRate),
|
|
223
245
|
condition: (s) => !!s.minHeartRate,
|
|
224
246
|
},
|
|
@@ -234,6 +256,8 @@ exports.METRICS_REGISTRY = {
|
|
|
234
256
|
healthKitSource: 'HKQuantityTypeIdentifierActiveEnergyBurned',
|
|
235
257
|
isHealthKit: true,
|
|
236
258
|
availableFrom: { watch: true, manual: true },
|
|
259
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
260
|
+
hasChart: true,
|
|
237
261
|
formatter: (s) => String(s.calories),
|
|
238
262
|
condition: (s) => !!s.calories,
|
|
239
263
|
},
|
|
@@ -249,6 +273,8 @@ exports.METRICS_REGISTRY = {
|
|
|
249
273
|
healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
|
|
250
274
|
isHealthKit: true,
|
|
251
275
|
availableFrom: { watch: true, manual: false },
|
|
276
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
277
|
+
hasChart: true,
|
|
252
278
|
formatter: (s) => String(s.totalStrokeCount || 0),
|
|
253
279
|
condition: (s) => !!s.totalStrokeCount,
|
|
254
280
|
},
|
|
@@ -262,6 +288,8 @@ exports.METRICS_REGISTRY = {
|
|
|
262
288
|
isPerSession: true,
|
|
263
289
|
formula: 'Total Strokes ÷ (Active Time in Minutes)',
|
|
264
290
|
availableFrom: { watch: true, manual: true },
|
|
291
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
292
|
+
hasChart: true,
|
|
265
293
|
formatter: (s) => `${(0, exports.formatNumber)(s.avgStrokeRate, 1)} SPM`,
|
|
266
294
|
condition: (s) => !!s.avgStrokeRate,
|
|
267
295
|
},
|
|
@@ -275,6 +303,8 @@ exports.METRICS_REGISTRY = {
|
|
|
275
303
|
isPerSession: true,
|
|
276
304
|
formula: 'Distance ÷ Total Strokes',
|
|
277
305
|
availableFrom: { watch: true, manual: true },
|
|
306
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
307
|
+
hasChart: true,
|
|
278
308
|
formatter: (s) => `${(0, exports.formatNumber)(s.avgDistancePerStroke, 2)}m`,
|
|
279
309
|
condition: (s) => !!s.avgDistancePerStroke,
|
|
280
310
|
},
|
|
@@ -288,6 +318,8 @@ exports.METRICS_REGISTRY = {
|
|
|
288
318
|
isPerSession: true,
|
|
289
319
|
formula: 'Average of (Lap Time in Seconds + Lap Stroke Count) for each lap',
|
|
290
320
|
availableFrom: { watch: true, manual: true },
|
|
321
|
+
swimTypes: [SwimType.INDOOR],
|
|
322
|
+
hasChart: true,
|
|
291
323
|
formatter: (s) => (0, exports.formatNumber)(s.avgSwolfScore, 0),
|
|
292
324
|
condition: (s) => !!s.avgSwolfScore,
|
|
293
325
|
},
|
|
@@ -303,6 +335,7 @@ exports.METRICS_REGISTRY = {
|
|
|
303
335
|
healthKitSource: 'HKWorkoutEvent.metadata[HKMetadataKeySwimmingStrokeStyle]',
|
|
304
336
|
isHealthKit: true,
|
|
305
337
|
availableFrom: { watch: true, manual: true },
|
|
338
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
306
339
|
formatter: (s) => s.primaryStrokeType,
|
|
307
340
|
condition: (s) => !!s.primaryStrokeType,
|
|
308
341
|
},
|
|
@@ -316,6 +349,7 @@ exports.METRICS_REGISTRY = {
|
|
|
316
349
|
isPerLap: true,
|
|
317
350
|
source: 'Tracked per lap completion',
|
|
318
351
|
availableFrom: { watch: true, manual: false },
|
|
352
|
+
swimTypes: [SwimType.INDOOR],
|
|
319
353
|
formatter: (s) => `${s.lapSplitTimes.length} splits`,
|
|
320
354
|
lapFormatter: (v) => (0, exports.formatTime)(v),
|
|
321
355
|
condition: (s) => !!s.lapSplitTimes?.length,
|
|
@@ -333,6 +367,7 @@ exports.METRICS_REGISTRY = {
|
|
|
333
367
|
healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
|
|
334
368
|
isHealthKit: true,
|
|
335
369
|
availableFrom: { watch: true, manual: false },
|
|
370
|
+
swimTypes: [SwimType.INDOOR],
|
|
336
371
|
},
|
|
337
372
|
lapStrokeCounts: {
|
|
338
373
|
key: 'lapStrokeCounts',
|
|
@@ -347,6 +382,7 @@ exports.METRICS_REGISTRY = {
|
|
|
347
382
|
healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
|
|
348
383
|
isHealthKit: true,
|
|
349
384
|
availableFrom: { watch: true, manual: false },
|
|
385
|
+
swimTypes: [SwimType.INDOOR],
|
|
350
386
|
},
|
|
351
387
|
id: {
|
|
352
388
|
key: 'id',
|
|
@@ -355,6 +391,7 @@ exports.METRICS_REGISTRY = {
|
|
|
355
391
|
description: 'Session identifier',
|
|
356
392
|
isPerSession: true,
|
|
357
393
|
availableFrom: { watch: false, manual: false },
|
|
394
|
+
swimTypes: [SwimType.INDOOR],
|
|
358
395
|
},
|
|
359
396
|
watchSessionId: {
|
|
360
397
|
key: 'watchSessionId',
|
|
@@ -363,6 +400,7 @@ exports.METRICS_REGISTRY = {
|
|
|
363
400
|
description: 'Watch session identifier',
|
|
364
401
|
isPerSession: true,
|
|
365
402
|
availableFrom: { watch: true, manual: false },
|
|
403
|
+
swimTypes: [SwimType.INDOOR],
|
|
366
404
|
},
|
|
367
405
|
date: {
|
|
368
406
|
key: 'date',
|
|
@@ -371,6 +409,7 @@ exports.METRICS_REGISTRY = {
|
|
|
371
409
|
description: 'Session date',
|
|
372
410
|
isPerSession: true,
|
|
373
411
|
availableFrom: { watch: true, manual: true },
|
|
412
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
374
413
|
},
|
|
375
414
|
source: {
|
|
376
415
|
key: 'source',
|
|
@@ -379,20 +418,23 @@ exports.METRICS_REGISTRY = {
|
|
|
379
418
|
description: 'Data source',
|
|
380
419
|
isPerSession: true,
|
|
381
420
|
availableFrom: { watch: false, manual: false },
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
421
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
422
|
+
},
|
|
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
|
+
},*/
|
|
396
438
|
lapStrokeTypes: {
|
|
397
439
|
key: 'lapStrokeTypes',
|
|
398
440
|
label: 'Stroke',
|
|
@@ -401,6 +443,7 @@ exports.METRICS_REGISTRY = {
|
|
|
401
443
|
icon: 'list-outline',
|
|
402
444
|
isPerLap: true,
|
|
403
445
|
availableFrom: { watch: true, manual: false },
|
|
446
|
+
swimTypes: [SwimType.INDOOR],
|
|
404
447
|
formatter: (s) => `${s.lapStrokeTypes.length} types`,
|
|
405
448
|
condition: (s) => !!s.lapStrokeTypes?.length,
|
|
406
449
|
},
|
|
@@ -413,6 +456,7 @@ exports.METRICS_REGISTRY = {
|
|
|
413
456
|
isPerLap: true,
|
|
414
457
|
decimals: 1,
|
|
415
458
|
availableFrom: { watch: true, manual: false },
|
|
459
|
+
swimTypes: [SwimType.INDOOR],
|
|
416
460
|
},
|
|
417
461
|
lapCalories: {
|
|
418
462
|
key: 'lapCalories',
|
|
@@ -423,6 +467,7 @@ exports.METRICS_REGISTRY = {
|
|
|
423
467
|
isPerLap: true,
|
|
424
468
|
decimals: 0,
|
|
425
469
|
availableFrom: { watch: true, manual: false },
|
|
470
|
+
swimTypes: [SwimType.INDOOR],
|
|
426
471
|
},
|
|
427
472
|
lapDistances: {
|
|
428
473
|
key: 'lapDistances',
|
|
@@ -432,6 +477,7 @@ exports.METRICS_REGISTRY = {
|
|
|
432
477
|
description: 'Distance per lap',
|
|
433
478
|
isPerLap: true,
|
|
434
479
|
availableFrom: { watch: true, manual: false },
|
|
480
|
+
swimTypes: [SwimType.INDOOR],
|
|
435
481
|
},
|
|
436
482
|
lapSwolfScores: {
|
|
437
483
|
key: 'lapSwolfScores',
|
|
@@ -441,6 +487,7 @@ exports.METRICS_REGISTRY = {
|
|
|
441
487
|
isPerLap: true,
|
|
442
488
|
decimals: 0,
|
|
443
489
|
availableFrom: { watch: true, manual: false },
|
|
490
|
+
swimTypes: [SwimType.INDOOR],
|
|
444
491
|
},
|
|
445
492
|
lapStrokeRates: {
|
|
446
493
|
key: 'lapStrokeRates',
|
|
@@ -453,6 +500,7 @@ exports.METRICS_REGISTRY = {
|
|
|
453
500
|
decimals: 1,
|
|
454
501
|
formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
|
|
455
502
|
availableFrom: { watch: true, manual: false },
|
|
503
|
+
swimTypes: [SwimType.INDOOR],
|
|
456
504
|
},
|
|
457
505
|
lapDistancesPerStroke: {
|
|
458
506
|
key: 'lapDistancesPerStroke',
|
|
@@ -465,6 +513,7 @@ exports.METRICS_REGISTRY = {
|
|
|
465
513
|
decimals: 2,
|
|
466
514
|
formula: 'Lap Distance ÷ Lap Strokes',
|
|
467
515
|
availableFrom: { watch: true, manual: false },
|
|
516
|
+
swimTypes: [SwimType.INDOOR],
|
|
468
517
|
},
|
|
469
518
|
poolLocation: {
|
|
470
519
|
key: 'poolLocation',
|
|
@@ -473,6 +522,7 @@ exports.METRICS_REGISTRY = {
|
|
|
473
522
|
description: 'Pool location details',
|
|
474
523
|
isPerSession: true,
|
|
475
524
|
availableFrom: { watch: false, manual: true },
|
|
525
|
+
swimTypes: [SwimType.INDOOR],
|
|
476
526
|
},
|
|
477
527
|
notes: {
|
|
478
528
|
key: 'notes',
|
|
@@ -481,6 +531,7 @@ exports.METRICS_REGISTRY = {
|
|
|
481
531
|
description: 'Session notes',
|
|
482
532
|
isPerSession: true,
|
|
483
533
|
availableFrom: { watch: false, manual: true },
|
|
534
|
+
swimTypes: [SwimType.INDOOR],
|
|
484
535
|
},
|
|
485
536
|
trainingTypes: {
|
|
486
537
|
key: 'trainingTypes',
|
|
@@ -492,6 +543,7 @@ exports.METRICS_REGISTRY = {
|
|
|
492
543
|
availableFrom: { watch: false, manual: true },
|
|
493
544
|
formatter: (s) => s.trainingTypes?.join(', ') || '',
|
|
494
545
|
condition: (s) => !!s.trainingTypes?.length,
|
|
546
|
+
swimTypes: [SwimType.INDOOR],
|
|
495
547
|
},
|
|
496
548
|
perceivedEffort: {
|
|
497
549
|
key: 'perceivedEffort',
|
|
@@ -503,6 +555,7 @@ exports.METRICS_REGISTRY = {
|
|
|
503
555
|
availableFrom: { watch: false, manual: true },
|
|
504
556
|
formatter: (s) => s.perceivedEffort,
|
|
505
557
|
condition: (s) => !!s.perceivedEffort,
|
|
558
|
+
swimTypes: [SwimType.INDOOR],
|
|
506
559
|
},
|
|
507
560
|
equipment: {
|
|
508
561
|
key: 'equipment',
|
|
@@ -514,6 +567,56 @@ exports.METRICS_REGISTRY = {
|
|
|
514
567
|
availableFrom: { watch: false, manual: true },
|
|
515
568
|
formatter: (s) => s.equipment?.join(', ') || '',
|
|
516
569
|
condition: (s) => !!s.equipment?.length,
|
|
570
|
+
swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
|
|
571
|
+
},
|
|
572
|
+
routeCoordinates: {
|
|
573
|
+
key: 'routeCoordinates',
|
|
574
|
+
label: 'GPS Route',
|
|
575
|
+
category: MetricCategory.PERFORMANCE,
|
|
576
|
+
description: 'GPS coordinates of the swimming route',
|
|
577
|
+
icon: 'map-outline',
|
|
578
|
+
isPerSession: true,
|
|
579
|
+
availableFrom: { watch: true, manual: false },
|
|
580
|
+
swimTypes: [SwimType.OPEN_WATER],
|
|
581
|
+
formatter: (s) => s.routeCoordinates ? `${s.routeCoordinates.length} points` : '',
|
|
582
|
+
condition: (s) => !!s.routeCoordinates?.length,
|
|
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,
|
|
517
620
|
},
|
|
518
621
|
};
|
|
519
622
|
exports.MetricsHelper = {
|
|
@@ -521,5 +624,8 @@ exports.MetricsHelper = {
|
|
|
521
624
|
getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
|
|
522
625
|
getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
|
|
523
626
|
getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
|
|
627
|
+
getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(swimType)),
|
|
628
|
+
getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.INDOOR)),
|
|
629
|
+
getOpenWaterMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.OPEN_WATER)),
|
|
524
630
|
getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
|
|
525
631
|
};
|
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.3",
|
|
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",
|