@runaid/lactate-curve
v0.3.0
Published
Reusable React lactate curve chart component built on shadcn/ui charts. Generate approximate physiological lactate curves from baseline lactate, LT1, LT2, and optional VO2max.
Downloads
270
Maintainers
Readme
@runaid/lactate-curve
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:
baselineLactateLT1LT2- optional
VO2max
It is built with React, TypeScript, recharts, and shadcn-style chart primitives.
What It Models
This package is intentionally an approximate visualization layer, not a lab-grade physiology engine.
The generated curve is constrained to reflect the structure described in the accompanying articles:
- low and near-flat at easy intensity
- first meaningful rise around LT1
- steeper rise approaching LT2
- accelerating nonlinear rise beyond LT2
Thresholds are shown as meaningful landmarks on a continuous curve, not as discrete step changes.
Install
npm install @runaid/lactate-curve react react-dom rechartsBasic Usage
import { LactateCurveChart } from "@runaid/lactate-curve"
export function Example() {
return (
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
xAxisLabel="Running power (W)"
yAxisLabel="Blood lactate (mmol/L)"
/>
)
}Public API
type AnchorPoint = {
intensity: number
lactate: number
}
type IntensityAnchor = {
intensity: number
}
type XAxisCompression = {
endIntensity: number | "lt1" | "lt2" | "vo2max"
scale?: number
}
type ZoneBoundary =
| number
| "min"
| "max"
| "baseline"
| "lt1"
| "lt2"
| "vo2max"
type ZoneConfig = {
startIntensity?: ZoneBoundary
endIntensity?: ZoneBoundary
label: string
color: string
opacity?: number
}
type RaceMarker = {
intensity: number
label: string
color?: string
radius?: number
showLabel?: boolean
tooltip?: string
}
type ThresholdAnnotation = {
label?: string
color?: string
strokeDasharray?: string
lineWidth?: number
showGuide?: boolean
showCircle?: boolean
circleRadius?: number
}
type LactateCurveChartProps = {
baselineLactate: number
lt1: AnchorPoint
lt2: AnchorPoint
vo2max?: IntensityAnchor
zones?: ZoneConfig[]
raceMarkers?: RaceMarker[]
xAxisLabel?: string
yAxisLabel?: string
xAxisPrecision?: number
yAxisPrecision?: number
showHeader?: boolean
showXAxisTicks?: boolean
xAxisCompression?: XAxisCompression
xAxisCompressionNote?: string | null
title?: string
description?: string
showThresholdLabels?: boolean
showThresholdGuides?: boolean
showThresholdCircles?: boolean
showGrid?: boolean
showLegend?: boolean
thresholdAnnotations?: {
lt1?: ThresholdAnnotation
lt2?: ThresholdAnnotation
vo2max?: ThresholdAnnotation
}
theme?: {
curveColor?: string
axisColor?: string
gridColor?: string
labelColor?: string
thresholdColor?: string
fontFamily?: string
backgroundColor?: string
markerLabelColor?: string
}
icon?: ReactNode | ((props: SVGProps<SVGSVGElement>) => ReactNode)
labels?: {
lt1?: string
lt2?: string
vo2max?: string
curve?: string
zones?: string
raceMarkers?: string
}
minIntensity?: number
xAxisMax?: number
maxIntensity?: number
intensityStep?: number
curveSamples?: number
height?: number
className?: string
}Defaults
If you only provide baselineLactate, lt1, and lt2, the component will:
- render a clean lactate curve
- show LT1 and LT2 guides plus anchor circles
- default the x-axis label to
Intensity - default the y-axis label to
Blood lactate (mmol/L) - shade three physiological domains:
Moderate domain,Heavy domain,Severe domain
Pass zones={[]} to hide zones explicitly.
Examples
The package exports five example components directly:
BasicCurveExampleCurveWithVo2maxExampleDomainsOverlayExampleRaceMarkerOverlayExampleFullyThemedExample
You can also copy their prop configurations from src/examples.tsx.
1. Basic curve
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
/>2. Curve with VO2max
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
vo2max={{ intensity: 360 }}
xAxisMax={370}
xAxisCompression={{ endIntensity: "lt1", scale: 0.34 }}
intensityStep={0.5}
/>3. Domains overlay
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
vo2max={{ intensity: 355 }}
zones={[
{ startIntensity: "min", endIntensity: "lt1", label: "Moderate", color: "#22c55e" },
{ startIntensity: "lt1", endIntensity: "lt2", label: "Heavy", color: "#f59e0b" },
{ startIntensity: "lt2", endIntensity: "max", label: "Severe", color: "#ef4444" },
]}
/>4. Race marker overlay
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
vo2max={{ intensity: 360 }}
raceMarkers={[
{ intensity: 252, label: "Marathon" },
{ intensity: 300, label: "Half" },
{ intensity: 312, label: "10k" },
{ intensity: 329, label: "5k" },
{ intensity: 342, label: "3k" },
]}
/>5. Fully themed example
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
vo2max={{ intensity: 362 }}
title="Threshold Map"
description="A branded version for article embeds and product UI."
xAxisLabel="Power (W)"
yAxisLabel="Lactate (mmol/L)"
labels={{
lt1: "Aerobic threshold",
lt2: "Critical threshold",
vo2max: "VO2 ceiling",
}}
theme={{
curveColor: "#b91c1c",
thresholdColor: "#102a43",
axisColor: "#243b53",
gridColor: "#d9e2ec",
labelColor: "#102a43",
backgroundColor: "#f8fbff",
fontFamily: "Avenir Next, ui-sans-serif, system-ui, sans-serif",
}}
/>Utilities
The lower-level helpers are exported for custom workflows:
generateLactateCurveresolveZonesprojectMarkerToCurve
This makes it easy to:
- reuse the physiology approximation outside the default chart
- project custom markers or annotations onto the generated curve
- drive alternate legends, tables, or narrative callouts from the same data
Curve Guardrails
The generator enforces a few constraints to avoid obviously implausible shapes:
lt1.intensity < lt2.intensity < vo2max.intensitywhenvo2maxexists- lactate can dip slightly from resting baseline into very easy exercise before rising
- from LT1 onward, lactate is monotonically non-decreasing
- the post-LT2 segment continues rising even when
vo2maxis omitted
VO2max is treated as an intensity landmark only. Its y-position is always computed from the rendered lactate curve.
X-Axis Compression
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:
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
vo2max={{ intensity: 360 }}
xAxisCompression={{ endIntensity: "lt1", scale: 0.34 }}
/>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.
You can override or suppress the note shown below compressed charts:
<LactateCurveChart
baselineLactate={1.1}
lt1={{ intensity: 220, lactate: 1.9 }}
lt2={{ intensity: 305, lactate: 4.1 }}
xAxisCompression={{ endIntensity: "lt1", scale: 0.34 }}
xAxisCompressionNote={null}
/>Precision And Hover Resolution
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:
<LactateCurveChart
baselineLactate={1.7}
lt1={{ intensity: 15, lactate: 1.5 }}
lt2={{ intensity: 17, lactate: 4.3 }}
vo2max={{ intensity: 18.5 }}
xAxisPrecision={1}
yAxisPrecision={1}
intensityStep={0.5}
/>xAxisPrecision: decimals shown for x-axis ticks and x tooltip valuesyAxisPrecision: decimals shown for y-axis ticks and lactate tooltip valuesintensityStep: spacing between generated x-values on the curve, which determines hover granularityxAxisMax: explicit end-of-axis override; if omitted, the chart ends shortly afterVO2maxwhen present
Accessibility Notes
- The chart wrapper uses semantic
figure/figcaption - thresholds and markers are rendered with readable labels
- tooltips are keyboard-friendly when the host chart interaction is enabled
- theme props let consumers improve contrast for article and product contexts
Development
npm install
npm run check
npm test
npm run buildLocal Playground
For visual testing, this repository also includes a private Vite app in playground/.
Run it with:
npm --prefix playground install
npm run playgroundThis playground is not published to npm. The root package uses a files allowlist in package.json, so consumers only get the built library output and docs.
