@swimedge/metrics 1.0.4 → 1.0.6

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 CHANGED
@@ -6,19 +6,17 @@ export declare enum SwimType {
6
6
  INDOOR = "indoorPool",
7
7
  OPEN_WATER = "openWater"
8
8
  }
9
- export interface SwimSessionLaps {
10
- lapSplitTimes?: number[];
11
- lapStrokeTypes?: string[];
12
- lapHeartRates?: number[];
13
- lapStrokeCounts?: number[];
14
- lapPaces?: number[];
15
- lapCalories?: number[];
16
- lapDistances?: number[];
17
- lapSwolfScores?: number[];
18
- lapStrokeRates?: number[];
19
- lapDistancesPerStroke?: number[];
9
+ export interface SwimLap {
10
+ strokeCount?: number;
11
+ pace?: number;
12
+ strokeRate?: number;
13
+ distancePerStroke?: number;
14
+ strokeStyle?: string;
15
+ splitTime?: number;
16
+ swolfScore?: number;
17
+ calories?: number;
20
18
  }
21
- export interface SwimSession extends SwimSessionLaps {
19
+ export interface SwimSession {
22
20
  id: string;
23
21
  watchSessionId?: string;
24
22
  date: string;
@@ -54,10 +52,10 @@ export interface SwimSession extends SwimSessionLaps {
54
52
  equipment?: string[];
55
53
  avgDistancePerStroke?: number;
56
54
  routeCoordinates?: number[][];
57
- routeMapImage?: string;
58
55
  deviceType?: string;
59
56
  osVersion?: string;
60
57
  healthKitWorkoutId?: string;
58
+ lapData?: SwimLap[];
61
59
  }
62
60
  export interface AggregatedSwimMetrics {
63
61
  totalDistance: number;
@@ -106,7 +104,6 @@ export interface MetricDefinition {
106
104
  description: string;
107
105
  icon?: string;
108
106
  isPerSession?: boolean;
109
- isPerLap?: boolean;
110
107
  formula?: string;
111
108
  source?: string;
112
109
  healthKitSource?: string;
@@ -119,18 +116,39 @@ export interface MetricDefinition {
119
116
  swimTypes: SwimType[];
120
117
  hasChart?: boolean;
121
118
  formatter?: (session: SwimSession) => string | number | null;
122
- lapFormatter?: (value: any) => string;
123
119
  condition?: (session: SwimSession) => boolean;
124
120
  }
125
- export declare const METRICS_REGISTRY: Record<keyof SwimSession, MetricDefinition>;
121
+ export interface LapMetricDefinition {
122
+ key: keyof SwimLap;
123
+ label: string;
124
+ unit?: string;
125
+ category: MetricCategory;
126
+ description: string;
127
+ icon?: string;
128
+ formula?: string;
129
+ source?: string;
130
+ healthKitSource?: string;
131
+ isHealthKit?: boolean;
132
+ decimals?: number;
133
+ availableFrom: {
134
+ watch: boolean;
135
+ manual: boolean;
136
+ };
137
+ swimTypes: SwimType[];
138
+ lapFormatter?: (value: any) => string;
139
+ }
140
+ export declare const LAP_METRICS_REGISTRY: Record<keyof SwimLap, LapMetricDefinition>;
141
+ export declare const METRICS_REGISTRY: Partial<Record<keyof SwimSession, MetricDefinition>>;
126
142
  export declare const MetricsHelper: {
127
- getMetric: (key: keyof SwimSession) => MetricDefinition;
143
+ getMetric: (key: keyof SwimSession) => MetricDefinition | undefined;
128
144
  getSessionMetrics: () => MetricDefinition[];
129
145
  getChartMetrics: () => MetricDefinition[];
130
- getLapMetrics: () => MetricDefinition[];
146
+ getLapMetrics: () => LapMetricDefinition[];
147
+ getLapMetric: (key: keyof SwimLap) => LapMetricDefinition;
131
148
  getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
132
149
  getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
133
150
  getIndoorMetrics: () => MetricDefinition[];
134
151
  getOpenWaterMetrics: () => MetricDefinition[];
135
152
  getAllMetricKeys: () => string[];
153
+ getAllLapMetricKeys: () => string[];
136
154
  };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
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.formatSwimType = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
7
+ exports.MetricsHelper = exports.METRICS_REGISTRY = exports.LAP_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
10
  SwimType["INDOOR"] = "indoorPool";
@@ -31,12 +31,13 @@ const formatPace = (pace) => {
31
31
  };
32
32
  exports.formatPace = formatPace;
33
33
  const formatTime = (seconds) => {
34
- const mins = Math.floor(seconds / 60);
35
- const secs = seconds % 60;
34
+ const totalSeconds = Math.floor(seconds);
35
+ const mins = Math.floor(totalSeconds / 60);
36
+ const secs = totalSeconds % 60;
36
37
  if (mins >= 60) {
37
38
  const hours = Math.floor(mins / 60);
38
39
  const remainingMins = mins % 60;
39
- return `${hours}:${remainingMins.toString().padStart(2, '0')}`;
40
+ return `${hours}:${remainingMins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
40
41
  }
41
42
  return `${mins}:${secs.toString().padStart(2, '0')}`;
42
43
  };
@@ -60,6 +61,97 @@ const formatSwimType = (swimType) => {
60
61
  }[swimType] || swimType);
61
62
  };
62
63
  exports.formatSwimType = formatSwimType;
64
+ exports.LAP_METRICS_REGISTRY = {
65
+ strokeCount: {
66
+ key: 'strokeCount',
67
+ label: 'Strokes',
68
+ unit: '',
69
+ category: MetricCategory.TECHNIQUE,
70
+ description: 'Number of strokes in this lap',
71
+ icon: 'hand-left-outline',
72
+ decimals: 0,
73
+ source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount per lap',
74
+ healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
75
+ isHealthKit: true,
76
+ availableFrom: { watch: true, manual: false },
77
+ swimTypes: [SwimType.INDOOR],
78
+ },
79
+ pace: {
80
+ key: 'pace',
81
+ label: 'Pace',
82
+ unit: '/100m',
83
+ category: MetricCategory.PERFORMANCE,
84
+ description: 'Time per 100m for this lap',
85
+ decimals: 1,
86
+ availableFrom: { watch: true, manual: false },
87
+ swimTypes: [SwimType.INDOOR],
88
+ lapFormatter: (v) => (0, exports.formatPace)(v),
89
+ },
90
+ strokeRate: {
91
+ key: 'strokeRate',
92
+ label: 'SR',
93
+ unit: 'SPM',
94
+ category: MetricCategory.TECHNIQUE,
95
+ description: 'Strokes per minute for this lap',
96
+ icon: 'repeat-outline',
97
+ decimals: 1,
98
+ formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
99
+ availableFrom: { watch: true, manual: false },
100
+ swimTypes: [SwimType.INDOOR],
101
+ },
102
+ distancePerStroke: {
103
+ key: 'distancePerStroke',
104
+ label: 'DPS',
105
+ unit: 'm',
106
+ category: MetricCategory.TECHNIQUE,
107
+ description: 'Distance per stroke for this lap',
108
+ icon: 'arrow-forward-outline',
109
+ decimals: 2,
110
+ formula: 'Lap Distance ÷ Lap Strokes',
111
+ availableFrom: { watch: true, manual: false },
112
+ swimTypes: [SwimType.INDOOR],
113
+ },
114
+ strokeStyle: {
115
+ key: 'strokeStyle',
116
+ label: 'Stroke',
117
+ category: MetricCategory.TECHNIQUE,
118
+ description: 'Swimming stroke used in this lap',
119
+ icon: 'list-outline',
120
+ availableFrom: { watch: true, manual: false },
121
+ swimTypes: [SwimType.INDOOR],
122
+ },
123
+ splitTime: {
124
+ key: 'splitTime',
125
+ label: 'Time',
126
+ unit: 's',
127
+ category: MetricCategory.PERFORMANCE,
128
+ description: 'Time taken for this lap',
129
+ icon: 'stopwatch-outline',
130
+ source: 'Tracked per lap completion',
131
+ availableFrom: { watch: true, manual: false },
132
+ swimTypes: [SwimType.INDOOR],
133
+ lapFormatter: (v) => (0, exports.formatTime)(v),
134
+ },
135
+ swolfScore: {
136
+ key: 'swolfScore',
137
+ label: 'SWOLF',
138
+ category: MetricCategory.TECHNIQUE,
139
+ description: 'SWOLF score for this lap',
140
+ decimals: 0,
141
+ availableFrom: { watch: true, manual: false },
142
+ swimTypes: [SwimType.INDOOR],
143
+ },
144
+ calories: {
145
+ key: 'calories',
146
+ label: 'Cal',
147
+ unit: 'kcal',
148
+ category: MetricCategory.ENERGY,
149
+ description: 'Calories burned in this lap',
150
+ decimals: 0,
151
+ availableFrom: { watch: true, manual: false },
152
+ swimTypes: [SwimType.INDOOR],
153
+ },
154
+ };
63
155
  exports.METRICS_REGISTRY = {
64
156
  distance: {
65
157
  key: 'distance',
@@ -135,7 +227,6 @@ exports.METRICS_REGISTRY = {
135
227
  availableFrom: { watch: true, manual: true },
136
228
  swimTypes: [SwimType.INDOOR],
137
229
  hasChart: true,
138
- formatter: (s) => String(s.laps),
139
230
  condition: (s) => !!s.laps,
140
231
  },
141
232
  avgPace: {
@@ -193,6 +284,7 @@ exports.METRICS_REGISTRY = {
193
284
  isHealthKit: true,
194
285
  availableFrom: { watch: true, manual: false },
195
286
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
287
+ hasChart: true,
196
288
  formatter: (s) => `${(0, exports.formatNumber)(s.waterTemp, 1)}°C`,
197
289
  condition: (s) => !!s.waterTemp,
198
290
  },
@@ -209,6 +301,7 @@ exports.METRICS_REGISTRY = {
209
301
  isHealthKit: true,
210
302
  availableFrom: { watch: true, manual: false },
211
303
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
304
+ hasChart: true,
212
305
  formatter: (s) => (0, exports.formatHeartRate)(s.avgHeartRate),
213
306
  condition: (s) => !!s.avgHeartRate,
214
307
  },
@@ -225,6 +318,7 @@ exports.METRICS_REGISTRY = {
225
318
  isHealthKit: true,
226
319
  availableFrom: { watch: true, manual: false },
227
320
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
321
+ hasChart: true,
228
322
  formatter: (s) => (0, exports.formatHeartRate)(s.maxHeartRate),
229
323
  condition: (s) => !!s.maxHeartRate,
230
324
  },
@@ -241,6 +335,7 @@ exports.METRICS_REGISTRY = {
241
335
  isHealthKit: true,
242
336
  availableFrom: { watch: true, manual: false },
243
337
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
338
+ hasChart: true,
244
339
  formatter: (s) => (0, exports.formatHeartRate)(s.minHeartRate),
245
340
  condition: (s) => !!s.minHeartRate,
246
341
  },
@@ -339,51 +434,6 @@ exports.METRICS_REGISTRY = {
339
434
  formatter: (s) => s.primaryStrokeType,
340
435
  condition: (s) => !!s.primaryStrokeType,
341
436
  },
342
- lapSplitTimes: {
343
- key: 'lapSplitTimes',
344
- label: 'Time',
345
- unit: 's',
346
- category: MetricCategory.PERFORMANCE,
347
- description: 'Individual lap times recorded during the session.',
348
- icon: 'stopwatch-outline',
349
- isPerLap: true,
350
- source: 'Tracked per lap completion',
351
- availableFrom: { watch: true, manual: false },
352
- swimTypes: [SwimType.INDOOR],
353
- formatter: (s) => `${s.lapSplitTimes.length} splits`,
354
- lapFormatter: (v) => (0, exports.formatTime)(v),
355
- condition: (s) => !!s.lapSplitTimes?.length,
356
- },
357
- lapHeartRates: {
358
- key: 'lapHeartRates',
359
- label: 'HR',
360
- unit: 'bpm',
361
- category: MetricCategory.HEALTH,
362
- description: 'HR per lap',
363
- icon: 'heart-outline',
364
- isPerLap: true,
365
- decimals: 0,
366
- source: 'HealthKit: HKQuantityTypeIdentifierHeartRate sampled at lap completion',
367
- healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
368
- isHealthKit: true,
369
- availableFrom: { watch: true, manual: false },
370
- swimTypes: [SwimType.INDOOR],
371
- },
372
- lapStrokeCounts: {
373
- key: 'lapStrokeCounts',
374
- label: 'Strokes',
375
- unit: '',
376
- category: MetricCategory.TECHNIQUE,
377
- description: 'Strokes per lap',
378
- icon: 'hand-left-outline',
379
- isPerLap: true,
380
- decimals: 0,
381
- source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount per lap',
382
- healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
383
- isHealthKit: true,
384
- availableFrom: { watch: true, manual: false },
385
- swimTypes: [SwimType.INDOOR],
386
- },
387
437
  id: {
388
438
  key: 'id',
389
439
  label: 'ID',
@@ -420,101 +470,6 @@ exports.METRICS_REGISTRY = {
420
470
  availableFrom: { watch: false, manual: false },
421
471
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
422
472
  },
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
- },*/
438
- lapStrokeTypes: {
439
- key: 'lapStrokeTypes',
440
- label: 'Stroke',
441
- category: MetricCategory.TECHNIQUE,
442
- description: 'Swimming stroke used per lap',
443
- icon: 'list-outline',
444
- isPerLap: true,
445
- availableFrom: { watch: true, manual: false },
446
- swimTypes: [SwimType.INDOOR],
447
- formatter: (s) => `${s.lapStrokeTypes.length} types`,
448
- condition: (s) => !!s.lapStrokeTypes?.length,
449
- },
450
- lapPaces: {
451
- key: 'lapPaces',
452
- label: 'Pace',
453
- unit: '/100m',
454
- category: MetricCategory.PERFORMANCE,
455
- description: 'Pace per lap',
456
- isPerLap: true,
457
- decimals: 1,
458
- availableFrom: { watch: true, manual: false },
459
- swimTypes: [SwimType.INDOOR],
460
- },
461
- lapCalories: {
462
- key: 'lapCalories',
463
- label: 'Cal',
464
- unit: 'kcal',
465
- category: MetricCategory.ENERGY,
466
- description: 'Calories per lap',
467
- isPerLap: true,
468
- decimals: 0,
469
- availableFrom: { watch: true, manual: false },
470
- swimTypes: [SwimType.INDOOR],
471
- },
472
- lapDistances: {
473
- key: 'lapDistances',
474
- label: 'Lap Distances',
475
- unit: 'm',
476
- category: MetricCategory.PERFORMANCE,
477
- description: 'Distance per lap',
478
- isPerLap: true,
479
- availableFrom: { watch: true, manual: false },
480
- swimTypes: [SwimType.INDOOR],
481
- },
482
- lapSwolfScores: {
483
- key: 'lapSwolfScores',
484
- label: 'SWOLF',
485
- category: MetricCategory.TECHNIQUE,
486
- description: 'SWOLF per lap',
487
- isPerLap: true,
488
- decimals: 0,
489
- availableFrom: { watch: true, manual: false },
490
- swimTypes: [SwimType.INDOOR],
491
- },
492
- lapStrokeRates: {
493
- key: 'lapStrokeRates',
494
- label: 'SR',
495
- unit: 'SPM',
496
- category: MetricCategory.TECHNIQUE,
497
- description: 'Strokes per minute per lap',
498
- icon: 'repeat-outline',
499
- isPerLap: true,
500
- decimals: 1,
501
- formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
502
- availableFrom: { watch: true, manual: false },
503
- swimTypes: [SwimType.INDOOR],
504
- },
505
- lapDistancesPerStroke: {
506
- key: 'lapDistancesPerStroke',
507
- label: 'DPS',
508
- unit: 'm',
509
- category: MetricCategory.TECHNIQUE,
510
- description: 'Distance covered per stroke per lap',
511
- icon: 'arrow-forward-outline',
512
- isPerLap: true,
513
- decimals: 2,
514
- formula: 'Lap Distance ÷ Lap Strokes',
515
- availableFrom: { watch: true, manual: false },
516
- swimTypes: [SwimType.INDOOR],
517
- },
518
473
  poolLocation: {
519
474
  key: 'poolLocation',
520
475
  label: 'Pool Location',
@@ -608,25 +563,27 @@ exports.METRICS_REGISTRY = {
608
563
  availableFrom: { watch: true, manual: false },
609
564
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
610
565
  },
611
- routeMapImage: {
612
- key: 'routeMapImage',
613
- label: 'Route Map',
566
+ lapData: {
567
+ key: 'lapData',
568
+ label: 'Lap Data',
614
569
  category: MetricCategory.PERFORMANCE,
615
- description: 'Base64 encoded route map image for open water sessions',
570
+ description: 'Detailed data for each lap',
616
571
  isPerSession: true,
617
- availableFrom: { watch: false, manual: false },
618
- swimTypes: [SwimType.OPEN_WATER],
619
- condition: (s) => !!s.routeMapImage,
572
+ availableFrom: { watch: true, manual: false },
573
+ swimTypes: [SwimType.INDOOR],
574
+ condition: (s) => !!s.lapData?.length,
620
575
  },
621
576
  };
622
577
  exports.MetricsHelper = {
623
578
  getMetric: (key) => exports.METRICS_REGISTRY[key],
624
- getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
625
- getChartMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession && m.hasChart),
626
- getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
627
- getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
628
- getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(swimType)),
629
- getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.INDOOR)),
630
- getOpenWaterMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.OPEN_WATER)),
579
+ getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.isPerSession),
580
+ getChartMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.isPerSession && m?.hasChart),
581
+ getLapMetrics: () => Object.values(exports.LAP_METRICS_REGISTRY),
582
+ getLapMetric: (key) => exports.LAP_METRICS_REGISTRY[key],
583
+ getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.category === category),
584
+ getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(swimType)),
585
+ getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(SwimType.INDOOR)),
586
+ getOpenWaterMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(SwimType.OPEN_WATER)),
631
587
  getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
588
+ getAllLapMetricKeys: () => Object.keys(exports.LAP_METRICS_REGISTRY),
632
589
  };
@@ -35,7 +35,7 @@ describe('Metrics Formatting Functions', () => {
35
35
  it('should format time in seconds to mm:ss', () => {
36
36
  expect((0, index_1.formatTime)(125)).toBe('2:05');
37
37
  expect((0, index_1.formatTime)(90)).toBe('1:30');
38
- expect((0, index_1.formatTime)(3665)).toBe('61:05');
38
+ expect((0, index_1.formatTime)(3665)).toBe('1:01:05');
39
39
  });
40
40
  it('should handle zero', () => {
41
41
  expect((0, index_1.formatTime)(0)).toBe('0:00');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimedge/metrics",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -52,4 +52,4 @@
52
52
  "ts-jest": "^29.1.0",
53
53
  "typescript": "^5.0.0"
54
54
  }
55
- }
55
+ }