@runaid/lactate-curve 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simon Lindgren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # `@runaid/lactate-curve`
2
+
3
+ Reusable React lactate curve chart components for article embeds and product UI. The library renders a deterministic, smooth, physiologically plausible approximation of a blood lactate curve from a small set of anchor points:
4
+
5
+ - `baselineLactate`
6
+ - `LT1`
7
+ - `LT2`
8
+ - optional `VO2max`
9
+
10
+ It is built with React, TypeScript, `recharts`, and shadcn-style chart primitives.
11
+
12
+ ## What It Models
13
+
14
+ This package is intentionally an approximate visualization layer, not a lab-grade physiology engine.
15
+
16
+ The generated curve is constrained to reflect the structure described in the accompanying articles:
17
+
18
+ - low and near-flat at easy intensity
19
+ - first meaningful rise around LT1
20
+ - steeper rise approaching LT2
21
+ - accelerating nonlinear rise beyond LT2
22
+
23
+ Thresholds are shown as meaningful landmarks on a continuous curve, not as discrete step changes.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install @runaid/lactate-curve react react-dom recharts
29
+ ```
30
+
31
+ ## Basic Usage
32
+
33
+ ```tsx
34
+ import { LactateCurveChart } from "@runaid/lactate-curve"
35
+
36
+ export function Example() {
37
+ return (
38
+ <LactateCurveChart
39
+ baselineLactate={1.1}
40
+ lt1={{ intensity: 220, lactate: 1.9 }}
41
+ lt2={{ intensity: 305, lactate: 4.1 }}
42
+ xAxisLabel="Running power (W)"
43
+ yAxisLabel="Blood lactate (mmol/L)"
44
+ />
45
+ )
46
+ }
47
+ ```
48
+
49
+ ## Public API
50
+
51
+ ```ts
52
+ type AnchorPoint = {
53
+ intensity: number
54
+ lactate: number
55
+ }
56
+
57
+ type IntensityAnchor = {
58
+ intensity: number
59
+ }
60
+
61
+ type XAxisCompression = {
62
+ endIntensity: number | "lt1" | "lt2" | "vo2max"
63
+ scale?: number
64
+ }
65
+
66
+ type ZoneBoundary =
67
+ | number
68
+ | "min"
69
+ | "max"
70
+ | "baseline"
71
+ | "lt1"
72
+ | "lt2"
73
+ | "vo2max"
74
+
75
+ type ZoneConfig = {
76
+ startIntensity?: ZoneBoundary
77
+ endIntensity?: ZoneBoundary
78
+ label: string
79
+ color: string
80
+ opacity?: number
81
+ }
82
+
83
+ type RaceMarker = {
84
+ intensity: number
85
+ label: string
86
+ color?: string
87
+ radius?: number
88
+ showLabel?: boolean
89
+ tooltip?: string
90
+ }
91
+
92
+ type ThresholdAnnotation = {
93
+ label?: string
94
+ color?: string
95
+ strokeDasharray?: string
96
+ lineWidth?: number
97
+ showGuide?: boolean
98
+ showCircle?: boolean
99
+ circleRadius?: number
100
+ }
101
+
102
+ type LactateCurveChartProps = {
103
+ baselineLactate: number
104
+ lt1: AnchorPoint
105
+ lt2: AnchorPoint
106
+ vo2max?: IntensityAnchor
107
+ zones?: ZoneConfig[]
108
+ raceMarkers?: RaceMarker[]
109
+ xAxisLabel?: string
110
+ yAxisLabel?: string
111
+ xAxisPrecision?: number
112
+ yAxisPrecision?: number
113
+ showXAxisTicks?: boolean
114
+ xAxisCompression?: XAxisCompression
115
+ title?: string
116
+ description?: string
117
+ showThresholdLabels?: boolean
118
+ showThresholdGuides?: boolean
119
+ showThresholdCircles?: boolean
120
+ showGrid?: boolean
121
+ showLegend?: boolean
122
+ thresholdAnnotations?: {
123
+ lt1?: ThresholdAnnotation
124
+ lt2?: ThresholdAnnotation
125
+ vo2max?: ThresholdAnnotation
126
+ }
127
+ theme?: {
128
+ curveColor?: string
129
+ axisColor?: string
130
+ gridColor?: string
131
+ labelColor?: string
132
+ thresholdColor?: string
133
+ fontFamily?: string
134
+ backgroundColor?: string
135
+ markerLabelColor?: string
136
+ }
137
+ icon?: ReactNode | ((props: SVGProps<SVGSVGElement>) => ReactNode)
138
+ labels?: {
139
+ lt1?: string
140
+ lt2?: string
141
+ vo2max?: string
142
+ curve?: string
143
+ zones?: string
144
+ raceMarkers?: string
145
+ }
146
+ minIntensity?: number
147
+ xAxisMax?: number
148
+ maxIntensity?: number
149
+ intensityStep?: number
150
+ curveSamples?: number
151
+ height?: number
152
+ className?: string
153
+ }
154
+ ```
155
+
156
+ ## Defaults
157
+
158
+ If you only provide `baselineLactate`, `lt1`, and `lt2`, the component will:
159
+
160
+ - render a clean lactate curve
161
+ - show LT1 and LT2 guides plus anchor circles
162
+ - default the x-axis label to `Intensity`
163
+ - default the y-axis label to `Blood lactate (mmol/L)`
164
+ - shade three physiological domains: `Moderate domain`, `Heavy domain`, `Severe domain`
165
+
166
+ Pass `zones={[]}` to hide zones explicitly.
167
+
168
+ ## Examples
169
+
170
+ The package exports five example components directly:
171
+
172
+ - `BasicCurveExample`
173
+ - `CurveWithVo2maxExample`
174
+ - `DomainsOverlayExample`
175
+ - `RaceMarkerOverlayExample`
176
+ - `FullyThemedExample`
177
+
178
+ You can also copy their prop configurations from [`src/examples.tsx`](./src/examples.tsx).
179
+
180
+ ### 1. Basic curve
181
+
182
+ ```tsx
183
+ <LactateCurveChart
184
+ baselineLactate={1.1}
185
+ lt1={{ intensity: 220, lactate: 1.9 }}
186
+ lt2={{ intensity: 305, lactate: 4.1 }}
187
+ />
188
+ ```
189
+
190
+ ### 2. Curve with VO2max
191
+
192
+ ```tsx
193
+ <LactateCurveChart
194
+ baselineLactate={1.1}
195
+ lt1={{ intensity: 220, lactate: 1.9 }}
196
+ lt2={{ intensity: 305, lactate: 4.1 }}
197
+ vo2max={{ intensity: 360 }}
198
+ xAxisMax={370}
199
+ xAxisCompression={{ endIntensity: "lt1", scale: 0.34 }}
200
+ intensityStep={0.5}
201
+ />
202
+ ```
203
+
204
+ ### 3. Domains overlay
205
+
206
+ ```tsx
207
+ <LactateCurveChart
208
+ baselineLactate={1.1}
209
+ lt1={{ intensity: 220, lactate: 1.9 }}
210
+ lt2={{ intensity: 305, lactate: 4.1 }}
211
+ vo2max={{ intensity: 355 }}
212
+ zones={[
213
+ { startIntensity: "min", endIntensity: "lt1", label: "Moderate", color: "#22c55e" },
214
+ { startIntensity: "lt1", endIntensity: "lt2", label: "Heavy", color: "#f59e0b" },
215
+ { startIntensity: "lt2", endIntensity: "max", label: "Severe", color: "#ef4444" },
216
+ ]}
217
+ />
218
+ ```
219
+
220
+ ### 4. Race marker overlay
221
+
222
+ ```tsx
223
+ <LactateCurveChart
224
+ baselineLactate={1.1}
225
+ lt1={{ intensity: 220, lactate: 1.9 }}
226
+ lt2={{ intensity: 305, lactate: 4.1 }}
227
+ vo2max={{ intensity: 360 }}
228
+ raceMarkers={[
229
+ { intensity: 252, label: "Marathon" },
230
+ { intensity: 300, label: "Half" },
231
+ { intensity: 312, label: "10k" },
232
+ { intensity: 329, label: "5k" },
233
+ { intensity: 342, label: "3k" },
234
+ ]}
235
+ />
236
+ ```
237
+
238
+ ### 5. Fully themed example
239
+
240
+ ```tsx
241
+ <LactateCurveChart
242
+ baselineLactate={1.1}
243
+ lt1={{ intensity: 220, lactate: 1.9 }}
244
+ lt2={{ intensity: 305, lactate: 4.1 }}
245
+ vo2max={{ intensity: 362 }}
246
+ title="Threshold Map"
247
+ description="A branded version for article embeds and product UI."
248
+ xAxisLabel="Power (W)"
249
+ yAxisLabel="Lactate (mmol/L)"
250
+ labels={{
251
+ lt1: "Aerobic threshold",
252
+ lt2: "Critical threshold",
253
+ vo2max: "VO2 ceiling",
254
+ }}
255
+ theme={{
256
+ curveColor: "#b91c1c",
257
+ thresholdColor: "#102a43",
258
+ axisColor: "#243b53",
259
+ gridColor: "#d9e2ec",
260
+ labelColor: "#102a43",
261
+ backgroundColor: "#f8fbff",
262
+ fontFamily: "Avenir Next, ui-sans-serif, system-ui, sans-serif",
263
+ }}
264
+ />
265
+ ```
266
+
267
+ ## Utilities
268
+
269
+ The lower-level helpers are exported for custom workflows:
270
+
271
+ - `generateLactateCurve`
272
+ - `resolveZones`
273
+ - `projectMarkerToCurve`
274
+
275
+ This makes it easy to:
276
+
277
+ - reuse the physiology approximation outside the default chart
278
+ - project custom markers or annotations onto the generated curve
279
+ - drive alternate legends, tables, or narrative callouts from the same data
280
+
281
+ ## Curve Guardrails
282
+
283
+ The generator enforces a few constraints to avoid obviously implausible shapes:
284
+
285
+ - `lt1.intensity < lt2.intensity < vo2max.intensity` when `vo2max` exists
286
+ - lactate can dip slightly from resting baseline into very easy exercise before rising
287
+ - from LT1 onward, lactate is monotonically non-decreasing
288
+ - the post-LT2 segment continues rising even when `vo2max` is omitted
289
+
290
+ `VO2max` is treated as an intensity landmark only. Its y-position is always computed from the rendered lactate curve.
291
+
292
+ ## X-Axis Compression
293
+
294
+ When the easy domain takes up too much horizontal space, you can visually compress the low-intensity part of the x-axis without changing the underlying intensities:
295
+
296
+ ```tsx
297
+ <LactateCurveChart
298
+ baselineLactate={1.1}
299
+ lt1={{ intensity: 220, lactate: 1.9 }}
300
+ lt2={{ intensity: 305, lactate: 4.1 }}
301
+ vo2max={{ intensity: 360 }}
302
+ xAxisCompression={{ endIntensity: "lt1", scale: 0.34 }}
303
+ />
304
+ ```
305
+
306
+ This compresses the chart region from the minimum x-value up to `LT1`, which leaves more horizontal space for the area around and above the thresholds while keeping tooltips and tick labels in raw intensity units.
307
+
308
+ ## Precision And Hover Resolution
309
+
310
+ By default, both axes and tooltip values are formatted to 1 decimal place. You can override that and also control the x-resolution used for hoverable curve points:
311
+
312
+ ```tsx
313
+ <LactateCurveChart
314
+ baselineLactate={1.7}
315
+ lt1={{ intensity: 15, lactate: 1.5 }}
316
+ lt2={{ intensity: 17, lactate: 4.3 }}
317
+ vo2max={{ intensity: 18.5 }}
318
+ xAxisPrecision={1}
319
+ yAxisPrecision={1}
320
+ intensityStep={0.5}
321
+ />
322
+ ```
323
+
324
+ - `xAxisPrecision`: decimals shown for x-axis ticks and x tooltip values
325
+ - `yAxisPrecision`: decimals shown for y-axis ticks and lactate tooltip values
326
+ - `intensityStep`: spacing between generated x-values on the curve, which determines hover granularity
327
+ - `xAxisMax`: explicit end-of-axis override; if omitted, the chart ends shortly after `VO2max` when present
328
+
329
+ ## Accessibility Notes
330
+
331
+ - The chart wrapper uses semantic `figure` / `figcaption`
332
+ - thresholds and markers are rendered with readable labels
333
+ - tooltips are keyboard-friendly when the host chart interaction is enabled
334
+ - theme props let consumers improve contrast for article and product contexts
335
+
336
+ ## Development
337
+
338
+ ```bash
339
+ npm install
340
+ npm run check
341
+ npm test
342
+ npm run build
343
+ ```
344
+
345
+ ## Local Playground
346
+
347
+ For visual testing, this repository also includes a private Vite app in [`playground/`](/Users/simonlindgren/workspace/runaid/lactate-curve/playground).
348
+
349
+ Run it with:
350
+
351
+ ```bash
352
+ npm --prefix playground install
353
+ npm run playground
354
+ ```
355
+
356
+ This playground is not published to npm. The root package uses a `files` allowlist in [`package.json`](/Users/simonlindgren/workspace/runaid/lactate-curve/package.json), so consumers only get the built library output and docs.