@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 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 = "indoor",
7
- OPEN_WATER = "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 formatAverageTurnTime: (session: SwimSession) => string;
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.formatAverageTurnTime = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
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"] = "indoor";
11
- SwimType["OPEN_WATER"] = "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 = Math.floor(seconds % 60);
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 formatAverageTurnTime = (session) => {
50
- if (!session.lapTurnTimes?.length)
51
- return '';
52
- const validTurnTimes = session.lapTurnTimes.filter((t) => !isNaN(t) && t > 0);
53
- if (!validTurnTimes.length)
54
- return '';
55
- const avgTime = validTurnTimes.reduce((a, b) => a + b, 0) / validTurnTimes.length;
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.formatAverageTurnTime = formatAverageTurnTime;
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
- lapTurnTimes: {
409
- key: 'lapTurnTimes',
410
- label: 'Turn',
411
- unit: 's',
412
- category: MetricCategory.TECHNIQUE,
413
- description: 'Time spent turning at pool wall per lap',
414
- icon: 'refresh-circle-outline',
415
- isPerLap: true,
416
- decimals: 2,
417
- availableFrom: { watch: true, manual: false },
418
- swimTypes: [SwimType.INDOOR],
419
- formatter: exports.formatAverageTurnTime,
420
- condition: (s) => !!s.lapTurnTimes?.length,
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)),
@@ -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.2",
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",