@swimedge/metrics 1.0.5 → 1.0.7

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
+ lapNumber?: number;
11
+ strokeCount?: number;
12
+ pace?: number;
13
+ strokeRate?: number;
14
+ distancePerStroke?: number;
15
+ strokeStyle?: string;
16
+ splitTime?: number;
17
+ swolfScore?: 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,8 @@ export interface SwimSession extends SwimSessionLaps {
57
55
  deviceType?: string;
58
56
  osVersion?: string;
59
57
  healthKitWorkoutId?: string;
58
+ lapData?: SwimLap[];
59
+ swimLaps?: SwimLap[];
60
60
  }
61
61
  export interface AggregatedSwimMetrics {
62
62
  totalDistance: number;
@@ -105,7 +105,6 @@ export interface MetricDefinition {
105
105
  description: string;
106
106
  icon?: string;
107
107
  isPerSession?: boolean;
108
- isPerLap?: boolean;
109
108
  formula?: string;
110
109
  source?: string;
111
110
  healthKitSource?: string;
@@ -118,18 +117,39 @@ export interface MetricDefinition {
118
117
  swimTypes: SwimType[];
119
118
  hasChart?: boolean;
120
119
  formatter?: (session: SwimSession) => string | number | null;
121
- lapFormatter?: (value: any) => string;
122
120
  condition?: (session: SwimSession) => boolean;
123
121
  }
124
- export declare const METRICS_REGISTRY: Record<keyof SwimSession, MetricDefinition>;
122
+ export interface LapMetricDefinition {
123
+ key: keyof SwimLap;
124
+ label: string;
125
+ unit?: string;
126
+ category: MetricCategory;
127
+ description: string;
128
+ icon?: string;
129
+ formula?: string;
130
+ source?: string;
131
+ healthKitSource?: string;
132
+ isHealthKit?: boolean;
133
+ decimals?: number;
134
+ availableFrom: {
135
+ watch: boolean;
136
+ manual: boolean;
137
+ };
138
+ swimTypes: SwimType[];
139
+ lapFormatter?: (value: any) => string;
140
+ }
141
+ export declare const LAP_METRICS_REGISTRY: Record<keyof SwimLap, LapMetricDefinition>;
142
+ export declare const METRICS_REGISTRY: Partial<Record<keyof SwimSession, MetricDefinition>>;
125
143
  export declare const MetricsHelper: {
126
- getMetric: (key: keyof SwimSession) => MetricDefinition;
144
+ getMetric: (key: keyof SwimSession) => MetricDefinition | undefined;
127
145
  getSessionMetrics: () => MetricDefinition[];
128
146
  getChartMetrics: () => MetricDefinition[];
129
- getLapMetrics: () => MetricDefinition[];
147
+ getLapMetrics: () => LapMetricDefinition[];
148
+ getLapMetric: (key: keyof SwimLap) => LapMetricDefinition;
130
149
  getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
131
150
  getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
132
151
  getIndoorMetrics: () => MetricDefinition[];
133
152
  getOpenWaterMetrics: () => MetricDefinition[];
134
153
  getAllMetricKeys: () => string[];
154
+ getAllLapMetricKeys: () => string[];
135
155
  };
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,106 @@ const formatSwimType = (swimType) => {
60
61
  }[swimType] || swimType);
61
62
  };
62
63
  exports.formatSwimType = formatSwimType;
64
+ exports.LAP_METRICS_REGISTRY = {
65
+ lapNumber: {
66
+ key: 'lapNumber',
67
+ label: 'Lap #',
68
+ category: MetricCategory.PERFORMANCE,
69
+ description: 'Lap number in the session',
70
+ decimals: 0,
71
+ availableFrom: { watch: true, manual: false },
72
+ swimTypes: [SwimType.INDOOR],
73
+ },
74
+ strokeCount: {
75
+ key: 'strokeCount',
76
+ label: 'Strokes',
77
+ unit: '',
78
+ category: MetricCategory.TECHNIQUE,
79
+ description: 'Number of strokes in this lap',
80
+ icon: 'hand-left-outline',
81
+ decimals: 0,
82
+ source: 'HealthKit: HKQuantityTypeIdentifierSwimmingStrokeCount per lap',
83
+ healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
84
+ isHealthKit: true,
85
+ availableFrom: { watch: true, manual: false },
86
+ swimTypes: [SwimType.INDOOR],
87
+ },
88
+ pace: {
89
+ key: 'pace',
90
+ label: 'Pace',
91
+ unit: '/100m',
92
+ category: MetricCategory.PERFORMANCE,
93
+ description: 'Time per 100m for this lap',
94
+ decimals: 1,
95
+ availableFrom: { watch: true, manual: false },
96
+ swimTypes: [SwimType.INDOOR],
97
+ lapFormatter: (v) => (0, exports.formatPace)(v),
98
+ },
99
+ strokeRate: {
100
+ key: 'strokeRate',
101
+ label: 'SR',
102
+ unit: 'SPM',
103
+ category: MetricCategory.TECHNIQUE,
104
+ description: 'Strokes per minute for this lap',
105
+ icon: 'repeat-outline',
106
+ decimals: 1,
107
+ formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
108
+ availableFrom: { watch: true, manual: false },
109
+ swimTypes: [SwimType.INDOOR],
110
+ },
111
+ distancePerStroke: {
112
+ key: 'distancePerStroke',
113
+ label: 'DPS',
114
+ unit: 'm',
115
+ category: MetricCategory.TECHNIQUE,
116
+ description: 'Distance per stroke for this lap',
117
+ icon: 'arrow-forward-outline',
118
+ decimals: 2,
119
+ formula: 'Lap Distance ÷ Lap Strokes',
120
+ availableFrom: { watch: true, manual: false },
121
+ swimTypes: [SwimType.INDOOR],
122
+ },
123
+ strokeStyle: {
124
+ key: 'strokeStyle',
125
+ label: 'Stroke',
126
+ category: MetricCategory.TECHNIQUE,
127
+ description: 'Swimming stroke used in this lap',
128
+ icon: 'list-outline',
129
+ availableFrom: { watch: true, manual: false },
130
+ swimTypes: [SwimType.INDOOR],
131
+ },
132
+ splitTime: {
133
+ key: 'splitTime',
134
+ label: 'Time',
135
+ unit: 's',
136
+ category: MetricCategory.PERFORMANCE,
137
+ description: 'Time taken for this lap',
138
+ icon: 'stopwatch-outline',
139
+ source: 'Tracked per lap completion',
140
+ availableFrom: { watch: true, manual: false },
141
+ swimTypes: [SwimType.INDOOR],
142
+ lapFormatter: (v) => (0, exports.formatTime)(v),
143
+ },
144
+ swolfScore: {
145
+ key: 'swolfScore',
146
+ label: 'SWOLF',
147
+ category: MetricCategory.TECHNIQUE,
148
+ description: 'SWOLF score for this lap',
149
+ decimals: 0,
150
+ availableFrom: { watch: true, manual: false },
151
+ swimTypes: [SwimType.INDOOR],
152
+ },
153
+ // calories: {
154
+ // key: 'calories',
155
+ // label: 'Cal',
156
+ // unit: 'kcal',
157
+ // category: MetricCategory.ENERGY,
158
+ // description: 'Calories burned in this lap',
159
+ // decimals: 0,
160
+ // availableFrom: { watch: true, manual: false },
161
+ // swimTypes: [SwimType.INDOOR],
162
+ // },
163
+ };
63
164
  exports.METRICS_REGISTRY = {
64
165
  distance: {
65
166
  key: 'distance',
@@ -135,7 +236,7 @@ exports.METRICS_REGISTRY = {
135
236
  availableFrom: { watch: true, manual: true },
136
237
  swimTypes: [SwimType.INDOOR],
137
238
  hasChart: true,
138
- formatter: (s) => String(s.laps),
239
+ formatter: (s) => String(s.laps || 0),
139
240
  condition: (s) => !!s.laps,
140
241
  },
141
242
  avgPace: {
@@ -343,51 +444,6 @@ exports.METRICS_REGISTRY = {
343
444
  formatter: (s) => s.primaryStrokeType,
344
445
  condition: (s) => !!s.primaryStrokeType,
345
446
  },
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
447
  id: {
392
448
  key: 'id',
393
449
  label: 'ID',
@@ -424,101 +480,6 @@ exports.METRICS_REGISTRY = {
424
480
  availableFrom: { watch: false, manual: false },
425
481
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
426
482
  },
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
483
  poolLocation: {
523
484
  key: 'poolLocation',
524
485
  label: 'Pool Location',
@@ -612,15 +573,27 @@ exports.METRICS_REGISTRY = {
612
573
  availableFrom: { watch: true, manual: false },
613
574
  swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
614
575
  },
576
+ lapData: {
577
+ key: 'lapData',
578
+ label: 'Lap Data',
579
+ category: MetricCategory.PERFORMANCE,
580
+ description: 'Detailed data for each lap',
581
+ isPerSession: true,
582
+ availableFrom: { watch: true, manual: false },
583
+ swimTypes: [SwimType.INDOOR],
584
+ condition: (s) => !!s.lapData?.length,
585
+ },
615
586
  };
616
587
  exports.MetricsHelper = {
617
588
  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)),
589
+ getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.isPerSession),
590
+ getChartMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.isPerSession && m?.hasChart),
591
+ getLapMetrics: () => Object.values(exports.LAP_METRICS_REGISTRY),
592
+ getLapMetric: (key) => exports.LAP_METRICS_REGISTRY[key],
593
+ getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.category === category),
594
+ getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(swimType)),
595
+ getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(SwimType.INDOOR)),
596
+ getOpenWaterMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m?.swimTypes?.includes(SwimType.OPEN_WATER)),
625
597
  getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
598
+ getAllLapMetricKeys: () => Object.keys(exports.LAP_METRICS_REGISTRY),
626
599
  };
@@ -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.7",
4
4
  "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",