@swimedge/metrics 1.0.5 → 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;
@@ -57,6 +55,7 @@ export interface SwimSession extends SwimSessionLaps {
57
55
  deviceType?: string;
58
56
  osVersion?: string;
59
57
  healthKitWorkoutId?: string;
58
+ lapData?: SwimLap[];
60
59
  }
61
60
  export interface AggregatedSwimMetrics {
62
61
  totalDistance: number;
@@ -105,7 +104,6 @@ export interface MetricDefinition {
105
104
  description: string;
106
105
  icon?: string;
107
106
  isPerSession?: boolean;
108
- isPerLap?: boolean;
109
107
  formula?: string;
110
108
  source?: string;
111
109
  healthKitSource?: string;
@@ -118,18 +116,39 @@ export interface MetricDefinition {
118
116
  swimTypes: SwimType[];
119
117
  hasChart?: boolean;
120
118
  formatter?: (session: SwimSession) => string | number | null;
121
- lapFormatter?: (value: any) => string;
122
119
  condition?: (session: SwimSession) => boolean;
123
120
  }
124
- 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>>;
125
142
  export declare const MetricsHelper: {
126
- getMetric: (key: keyof SwimSession) => MetricDefinition;
143
+ getMetric: (key: keyof SwimSession) => MetricDefinition | undefined;
127
144
  getSessionMetrics: () => MetricDefinition[];
128
145
  getChartMetrics: () => MetricDefinition[];
129
- getLapMetrics: () => MetricDefinition[];
146
+ getLapMetrics: () => LapMetricDefinition[];
147
+ getLapMetric: (key: keyof SwimLap) => LapMetricDefinition;
130
148
  getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
131
149
  getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
132
150
  getIndoorMetrics: () => MetricDefinition[];
133
151
  getOpenWaterMetrics: () => MetricDefinition[];
134
152
  getAllMetricKeys: () => string[];
153
+ getAllLapMetricKeys: () => string[];
135
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: {
@@ -343,51 +434,6 @@ exports.METRICS_REGISTRY = {
343
434
  formatter: (s) => s.primaryStrokeType,
344
435
  condition: (s) => !!s.primaryStrokeType,
345
436
  },
346
- lapSplitTimes: {
347
- key: 'lapSplitTimes',
348
- label: 'Time',
349
- unit: 's',
350
- category: MetricCategory.PERFORMANCE,
351
- description: 'Individual lap times recorded during the session.',
352
- icon: 'stopwatch-outline',
353
- isPerLap: true,
354
- source: 'Tracked per lap completion',
355
- availableFrom: { watch: true, manual: false },
356
- swimTypes: [SwimType.INDOOR],
357
- formatter: (s) => `${s.lapSplitTimes.length} splits`,
358
- lapFormatter: (v) => (0, exports.formatTime)(v),
359
- condition: (s) => !!s.lapSplitTimes?.length,
360
- },
361
- lapHeartRates: {
362
- key: 'lapHeartRates',
363
- label: 'HR',
364
- unit: 'bpm',
365
- category: MetricCategory.HEALTH,
366
- description: 'HR per lap',
367
- icon: 'heart-outline',
368
- isPerLap: true,
369
- decimals: 0,
370
- source: 'HealthKit: HKQuantityTypeIdentifierHeartRate sampled at lap completion',
371
- healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
372
- isHealthKit: true,
373
- availableFrom: { watch: true, manual: false },
374
- swimTypes: [SwimType.INDOOR],
375
- },
376
- lapStrokeCounts: {
377
- key: 'lapStrokeCounts',
378
- label: 'Strokes',
379
- unit: '',
380
- category: MetricCategory.TECHNIQUE,
381
- description: 'Strokes per lap',
382
- icon: 'hand-left-outline',
383
- isPerLap: true,
384
- decimals: 0,
385
- source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount per lap',
386
- healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
387
- isHealthKit: true,
388
- availableFrom: { watch: true, manual: false },
389
- swimTypes: [SwimType.INDOOR],
390
- },
391
437
  id: {
392
438
  key: 'id',
393
439
  label: 'ID',
@@ -424,101 +470,6 @@ exports.METRICS_REGISTRY = {
424
470
  availableFrom: { watch: false, manual: false },
425
471
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
426
472
  },
427
- // HealthKit doesn't track turn times
428
- /*lapTurnTimes: {
429
- key: 'lapTurnTimes',
430
- label: 'Turn',
431
- unit: 's',
432
- category: MetricCategory.TECHNIQUE,
433
- description: 'Time spent turning at pool wall per lap',
434
- icon: 'refresh-circle-outline',
435
- isPerLap: true,
436
- decimals: 2,
437
- availableFrom: { watch: true, manual: false },
438
- swimTypes: [SwimType.INDOOR],
439
- formatter: formatAverageTurnTime,
440
- condition: (s) => !!s.lapTurnTimes?.length,
441
- },*/
442
- lapStrokeTypes: {
443
- key: 'lapStrokeTypes',
444
- label: 'Stroke',
445
- category: MetricCategory.TECHNIQUE,
446
- description: 'Swimming stroke used per lap',
447
- icon: 'list-outline',
448
- isPerLap: true,
449
- availableFrom: { watch: true, manual: false },
450
- swimTypes: [SwimType.INDOOR],
451
- formatter: (s) => `${s.lapStrokeTypes.length} types`,
452
- condition: (s) => !!s.lapStrokeTypes?.length,
453
- },
454
- lapPaces: {
455
- key: 'lapPaces',
456
- label: 'Pace',
457
- unit: '/100m',
458
- category: MetricCategory.PERFORMANCE,
459
- description: 'Pace per lap',
460
- isPerLap: true,
461
- decimals: 1,
462
- availableFrom: { watch: true, manual: false },
463
- swimTypes: [SwimType.INDOOR],
464
- },
465
- lapCalories: {
466
- key: 'lapCalories',
467
- label: 'Cal',
468
- unit: 'kcal',
469
- category: MetricCategory.ENERGY,
470
- description: 'Calories per lap',
471
- isPerLap: true,
472
- decimals: 0,
473
- availableFrom: { watch: true, manual: false },
474
- swimTypes: [SwimType.INDOOR],
475
- },
476
- lapDistances: {
477
- key: 'lapDistances',
478
- label: 'Lap Distances',
479
- unit: 'm',
480
- category: MetricCategory.PERFORMANCE,
481
- description: 'Distance per lap',
482
- isPerLap: true,
483
- availableFrom: { watch: true, manual: false },
484
- swimTypes: [SwimType.INDOOR],
485
- },
486
- lapSwolfScores: {
487
- key: 'lapSwolfScores',
488
- label: 'SWOLF',
489
- category: MetricCategory.TECHNIQUE,
490
- description: 'SWOLF per lap',
491
- isPerLap: true,
492
- decimals: 0,
493
- availableFrom: { watch: true, manual: false },
494
- swimTypes: [SwimType.INDOOR],
495
- },
496
- lapStrokeRates: {
497
- key: 'lapStrokeRates',
498
- label: 'SR',
499
- unit: 'SPM',
500
- category: MetricCategory.TECHNIQUE,
501
- description: 'Strokes per minute per lap',
502
- icon: 'repeat-outline',
503
- isPerLap: true,
504
- decimals: 1,
505
- formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
506
- availableFrom: { watch: true, manual: false },
507
- swimTypes: [SwimType.INDOOR],
508
- },
509
- lapDistancesPerStroke: {
510
- key: 'lapDistancesPerStroke',
511
- label: 'DPS',
512
- unit: 'm',
513
- category: MetricCategory.TECHNIQUE,
514
- description: 'Distance covered per stroke per lap',
515
- icon: 'arrow-forward-outline',
516
- isPerLap: true,
517
- decimals: 2,
518
- formula: 'Lap Distance ÷ Lap Strokes',
519
- availableFrom: { watch: true, manual: false },
520
- swimTypes: [SwimType.INDOOR],
521
- },
522
473
  poolLocation: {
523
474
  key: 'poolLocation',
524
475
  label: 'Pool Location',
@@ -612,15 +563,27 @@ exports.METRICS_REGISTRY = {
612
563
  availableFrom: { watch: true, manual: false },
613
564
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
614
565
  },
566
+ lapData: {
567
+ key: 'lapData',
568
+ label: 'Lap Data',
569
+ category: MetricCategory.PERFORMANCE,
570
+ description: 'Detailed data for each lap',
571
+ isPerSession: true,
572
+ availableFrom: { watch: true, manual: false },
573
+ swimTypes: [SwimType.INDOOR],
574
+ condition: (s) => !!s.lapData?.length,
575
+ },
615
576
  };
616
577
  exports.MetricsHelper = {
617
578
  getMetric: (key) => exports.METRICS_REGISTRY[key],
618
- getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
619
- getChartMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession && m.hasChart),
620
- getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
621
- getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
622
- getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(swimType)),
623
- getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.INDOOR)),
624
- 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)),
625
587
  getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
588
+ getAllLapMetricKeys: () => Object.keys(exports.LAP_METRICS_REGISTRY),
626
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.5",
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
+ }