@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 +21 -0
- package/README.md +356 -0
- package/dist/index.cjs +1051 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +1016 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BasicCurveExample: () => BasicCurveExample,
|
|
34
|
+
CurveWithVo2maxExample: () => CurveWithVo2maxExample,
|
|
35
|
+
DomainsOverlayExample: () => DomainsOverlayExample,
|
|
36
|
+
FullyThemedExample: () => FullyThemedExample,
|
|
37
|
+
LactateCurveChart: () => LactateCurveChart,
|
|
38
|
+
RaceMarkerOverlayExample: () => RaceMarkerOverlayExample,
|
|
39
|
+
generateLactateCurve: () => generateLactateCurve,
|
|
40
|
+
projectMarkerToCurve: () => projectMarkerToCurve,
|
|
41
|
+
resolveZones: () => resolveZones
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/components/LactateCurveChart.tsx
|
|
46
|
+
var React2 = __toESM(require("react"), 1);
|
|
47
|
+
var import_recharts2 = require("recharts");
|
|
48
|
+
|
|
49
|
+
// src/lib/generateLactateCurve.ts
|
|
50
|
+
var MAX_LT1_BASELINE_DIP = 0.45;
|
|
51
|
+
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
52
|
+
var round = (value) => Math.round(value * 1e3) / 1e3;
|
|
53
|
+
var assertFinitePositive = (value, label) => {
|
|
54
|
+
if (!Number.isFinite(value)) {
|
|
55
|
+
throw new Error(`${label} must be a finite number.`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var buildModerateDomainFunction = ({
|
|
59
|
+
minIntensity,
|
|
60
|
+
baselineLactate,
|
|
61
|
+
lt1
|
|
62
|
+
}) => {
|
|
63
|
+
const span = lt1.intensity - minIntensity;
|
|
64
|
+
const delta = lt1.lactate - baselineLactate;
|
|
65
|
+
if (span <= 0) {
|
|
66
|
+
throw new Error("Moderate domain span must be greater than 0.");
|
|
67
|
+
}
|
|
68
|
+
if (Math.abs(delta) < 1e-4) {
|
|
69
|
+
return () => baselineLactate;
|
|
70
|
+
}
|
|
71
|
+
if (delta > 0) {
|
|
72
|
+
return (intensity) => {
|
|
73
|
+
const t = clamp((intensity - minIntensity) / span, 0, 1);
|
|
74
|
+
return baselineLactate + delta * Math.pow(t, 4);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const dip = baselineLactate - lt1.lactate;
|
|
78
|
+
const dipAccent = 0.32;
|
|
79
|
+
return (intensity) => {
|
|
80
|
+
const t = clamp((intensity - minIntensity) / span, 0, 1);
|
|
81
|
+
return baselineLactate - dip * (t + dipAccent * 4 * t * (1 - t));
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var buildHeavyDomainFunction = ({
|
|
85
|
+
lt1,
|
|
86
|
+
lt2
|
|
87
|
+
}) => {
|
|
88
|
+
const span = lt2.intensity - lt1.intensity;
|
|
89
|
+
if (span <= 0) {
|
|
90
|
+
throw new Error("Heavy domain span must be greater than 0.");
|
|
91
|
+
}
|
|
92
|
+
const slope = (lt2.lactate - lt1.lactate) / span;
|
|
93
|
+
return {
|
|
94
|
+
slope,
|
|
95
|
+
project: (intensity) => lt1.lactate + slope * (intensity - lt1.intensity)
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
var buildSevereDomainFunction = ({
|
|
99
|
+
lt2,
|
|
100
|
+
maxIntensity,
|
|
101
|
+
heavySlope
|
|
102
|
+
}) => {
|
|
103
|
+
const span = maxIntensity - lt2.intensity;
|
|
104
|
+
if (span <= 0) {
|
|
105
|
+
throw new Error("Severe domain span must be greater than 0.");
|
|
106
|
+
}
|
|
107
|
+
const curvature = clamp(2.4 / span, 0.02, 0.9);
|
|
108
|
+
const initialSevereSlope = heavySlope * 1.4;
|
|
109
|
+
return {
|
|
110
|
+
curvature,
|
|
111
|
+
initialSevereSlope,
|
|
112
|
+
project: (intensity) => {
|
|
113
|
+
const delta = Math.max(0, intensity - lt2.intensity);
|
|
114
|
+
return lt2.lactate + initialSevereSlope / curvature * (Math.exp(curvature * delta) - 1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
var generateLactateCurve = ({
|
|
119
|
+
baselineLactate,
|
|
120
|
+
lt1,
|
|
121
|
+
lt2,
|
|
122
|
+
vo2max,
|
|
123
|
+
minIntensity = 0,
|
|
124
|
+
maxIntensity,
|
|
125
|
+
intensityStep,
|
|
126
|
+
samples = 80
|
|
127
|
+
}) => {
|
|
128
|
+
assertFinitePositive(baselineLactate, "baselineLactate");
|
|
129
|
+
assertFinitePositive(lt1.intensity, "lt1 intensity");
|
|
130
|
+
assertFinitePositive(lt1.lactate, "lt1 lactate");
|
|
131
|
+
assertFinitePositive(lt2.intensity, "lt2 intensity");
|
|
132
|
+
assertFinitePositive(lt2.lactate, "lt2 lactate");
|
|
133
|
+
if (baselineLactate <= 0) {
|
|
134
|
+
throw new Error("baselineLactate must be greater than 0.");
|
|
135
|
+
}
|
|
136
|
+
if (baselineLactate - lt1.lactate > MAX_LT1_BASELINE_DIP) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`LT1 lactate can sit slightly below baseline, but the dip must be <= ${MAX_LT1_BASELINE_DIP} mmol/L.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (lt1.intensity <= minIntensity) {
|
|
142
|
+
throw new Error("LT1 intensity must be above the minimum chart intensity.");
|
|
143
|
+
}
|
|
144
|
+
if (lt1.intensity >= lt2.intensity) {
|
|
145
|
+
throw new Error("LT1 intensity must be below LT2 intensity.");
|
|
146
|
+
}
|
|
147
|
+
if (lt1.lactate >= lt2.lactate) {
|
|
148
|
+
throw new Error("LT1 lactate must be below LT2 lactate.");
|
|
149
|
+
}
|
|
150
|
+
if (vo2max) {
|
|
151
|
+
assertFinitePositive(vo2max.intensity, "vo2max intensity");
|
|
152
|
+
if (lt2.intensity >= vo2max.intensity) {
|
|
153
|
+
throw new Error("VO2max intensity must be greater than LT2 intensity.");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const maxX = maxIntensity ?? round(
|
|
157
|
+
vo2max ? vo2max.intensity * 1.08 : lt2.intensity + Math.max((lt2.intensity - lt1.intensity) * 0.6, lt2.intensity * 0.12)
|
|
158
|
+
);
|
|
159
|
+
if (maxX <= lt2.intensity) {
|
|
160
|
+
throw new Error("maxIntensity must extend beyond LT2.");
|
|
161
|
+
}
|
|
162
|
+
const moderateDomain = buildModerateDomainFunction({
|
|
163
|
+
minIntensity,
|
|
164
|
+
baselineLactate,
|
|
165
|
+
lt1
|
|
166
|
+
});
|
|
167
|
+
const heavyDomain = buildHeavyDomainFunction({ lt1, lt2 });
|
|
168
|
+
const severeDomain = buildSevereDomainFunction({
|
|
169
|
+
lt2,
|
|
170
|
+
maxIntensity: maxX,
|
|
171
|
+
heavySlope: heavyDomain.slope
|
|
172
|
+
});
|
|
173
|
+
const points = [];
|
|
174
|
+
const pointCount = intensityStep ? Math.max(2, Math.floor((maxX - minIntensity) / intensityStep) + 1) : samples;
|
|
175
|
+
for (let index = 0; index < pointCount; index += 1) {
|
|
176
|
+
const intensity = index === pointCount - 1 ? maxX : intensityStep ? minIntensity + intensityStep * index : minIntensity + (maxX - minIntensity) * index / (pointCount - 1);
|
|
177
|
+
let lactate;
|
|
178
|
+
if (intensity <= lt1.intensity) {
|
|
179
|
+
lactate = moderateDomain(intensity);
|
|
180
|
+
} else if (intensity <= lt2.intensity) {
|
|
181
|
+
lactate = heavyDomain.project(intensity);
|
|
182
|
+
} else {
|
|
183
|
+
lactate = severeDomain.project(intensity);
|
|
184
|
+
}
|
|
185
|
+
points.push({
|
|
186
|
+
intensity: round(intensity),
|
|
187
|
+
lactate: round(lactate)
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const endPoint = points[points.length - 1];
|
|
191
|
+
const minimumModerateEstimate = lt1.lactate < baselineLactate ? lt1.lactate - (baselineLactate - lt1.lactate) * 0.12 : Math.min(baselineLactate, lt1.lactate);
|
|
192
|
+
return {
|
|
193
|
+
points,
|
|
194
|
+
anchors: {
|
|
195
|
+
baseline: { intensity: minIntensity, lactate: baselineLactate },
|
|
196
|
+
lt1,
|
|
197
|
+
lt2,
|
|
198
|
+
vo2max,
|
|
199
|
+
max: endPoint
|
|
200
|
+
},
|
|
201
|
+
domain: {
|
|
202
|
+
minIntensity,
|
|
203
|
+
maxIntensity: maxX,
|
|
204
|
+
minLactate: round(Math.max(0, minimumModerateEstimate - 0.12)),
|
|
205
|
+
maxLactate: round(endPoint.lactate * 1.08)
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/lib/chart-defaults.ts
|
|
211
|
+
var DEFAULT_THEME = {
|
|
212
|
+
curveColor: "#e11d48",
|
|
213
|
+
axisColor: "#334155",
|
|
214
|
+
gridColor: "#cbd5e1",
|
|
215
|
+
labelColor: "#0f172a",
|
|
216
|
+
thresholdColor: "#0f172a",
|
|
217
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
218
|
+
backgroundColor: "#ffffff",
|
|
219
|
+
markerLabelColor: "#0f172a"
|
|
220
|
+
};
|
|
221
|
+
var DEFAULT_ZONE_OPACITY = 0.12;
|
|
222
|
+
var getDefaultZones = () => [
|
|
223
|
+
{
|
|
224
|
+
startIntensity: "min",
|
|
225
|
+
endIntensity: "lt1",
|
|
226
|
+
label: "Moderate domain",
|
|
227
|
+
color: "#22c55e",
|
|
228
|
+
opacity: DEFAULT_ZONE_OPACITY
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
startIntensity: "lt1",
|
|
232
|
+
endIntensity: "lt2",
|
|
233
|
+
label: "Heavy domain",
|
|
234
|
+
color: "#f59e0b",
|
|
235
|
+
opacity: DEFAULT_ZONE_OPACITY
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
startIntensity: "lt2",
|
|
239
|
+
endIntensity: "max",
|
|
240
|
+
label: "Severe domain",
|
|
241
|
+
color: "#ef4444",
|
|
242
|
+
opacity: DEFAULT_ZONE_OPACITY
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
// src/lib/projectMarkerToCurve.ts
|
|
247
|
+
var projectIntensityToCurve = (intensity, points) => {
|
|
248
|
+
if (points.length < 2) {
|
|
249
|
+
throw new Error("At least two curve points are required to project onto the curve.");
|
|
250
|
+
}
|
|
251
|
+
const clampedIntensity = Math.min(
|
|
252
|
+
points[points.length - 1].intensity,
|
|
253
|
+
Math.max(points[0].intensity, intensity)
|
|
254
|
+
);
|
|
255
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
256
|
+
const current = points[index];
|
|
257
|
+
const next = points[index + 1];
|
|
258
|
+
if (clampedIntensity >= current.intensity && clampedIntensity <= next.intensity) {
|
|
259
|
+
const span = next.intensity - current.intensity || 1;
|
|
260
|
+
const ratio = (clampedIntensity - current.intensity) / span;
|
|
261
|
+
const lactate = current.lactate + (next.lactate - current.lactate) * ratio;
|
|
262
|
+
return {
|
|
263
|
+
intensity: clampedIntensity,
|
|
264
|
+
lactate: Math.round(lactate * 1e3) / 1e3
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
intensity: clampedIntensity,
|
|
270
|
+
lactate: points[points.length - 1].lactate
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
var projectMarkerToCurve = (marker, points) => ({
|
|
274
|
+
...marker,
|
|
275
|
+
...projectIntensityToCurve(marker.intensity, points)
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/lib/resolveZones.ts
|
|
279
|
+
var resolveBoundary = (boundary, anchors, fallback) => {
|
|
280
|
+
if (boundary === void 0) return fallback;
|
|
281
|
+
if (typeof boundary === "number") return boundary;
|
|
282
|
+
switch (boundary) {
|
|
283
|
+
case "min":
|
|
284
|
+
case "baseline":
|
|
285
|
+
return anchors.baseline.intensity;
|
|
286
|
+
case "lt1":
|
|
287
|
+
return anchors.lt1.intensity;
|
|
288
|
+
case "lt2":
|
|
289
|
+
return anchors.lt2.intensity;
|
|
290
|
+
case "vo2max":
|
|
291
|
+
return anchors.vo2max?.intensity ?? anchors.max.intensity;
|
|
292
|
+
case "max":
|
|
293
|
+
return anchors.max.intensity;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var resolveZones = ({
|
|
297
|
+
zones,
|
|
298
|
+
curve
|
|
299
|
+
}) => {
|
|
300
|
+
const sourceZones = zones ?? getDefaultZones();
|
|
301
|
+
return sourceZones.map((zone) => {
|
|
302
|
+
const startIntensity = resolveBoundary(
|
|
303
|
+
zone.startIntensity,
|
|
304
|
+
curve.anchors,
|
|
305
|
+
curve.domain.minIntensity
|
|
306
|
+
);
|
|
307
|
+
const endIntensity = resolveBoundary(
|
|
308
|
+
zone.endIntensity,
|
|
309
|
+
curve.anchors,
|
|
310
|
+
curve.domain.maxIntensity
|
|
311
|
+
);
|
|
312
|
+
return {
|
|
313
|
+
label: zone.label,
|
|
314
|
+
color: zone.color,
|
|
315
|
+
opacity: zone.opacity ?? DEFAULT_ZONE_OPACITY,
|
|
316
|
+
startIntensity,
|
|
317
|
+
endIntensity
|
|
318
|
+
};
|
|
319
|
+
}).filter((zone) => zone.endIntensity > zone.startIntensity);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/lib/utils.ts
|
|
323
|
+
var import_clsx = require("clsx");
|
|
324
|
+
var cn = (...inputs) => (0, import_clsx.clsx)(inputs);
|
|
325
|
+
|
|
326
|
+
// src/components/ui/chart.tsx
|
|
327
|
+
var React = __toESM(require("react"), 1);
|
|
328
|
+
var import_recharts = require("recharts");
|
|
329
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
330
|
+
var ChartContext = React.createContext(null);
|
|
331
|
+
var useChart = () => {
|
|
332
|
+
const context = React.useContext(ChartContext);
|
|
333
|
+
if (!context) {
|
|
334
|
+
throw new Error("Chart components must be used within a ChartContainer.");
|
|
335
|
+
}
|
|
336
|
+
return context;
|
|
337
|
+
};
|
|
338
|
+
var ChartContainer = React.forwardRef(
|
|
339
|
+
({ config = {}, className, style, height = 360, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChartContext.Provider, { value: { config }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
340
|
+
"div",
|
|
341
|
+
{
|
|
342
|
+
ref,
|
|
343
|
+
className: cn("w-full", className),
|
|
344
|
+
style: { width: "100%", height, ...style },
|
|
345
|
+
...props,
|
|
346
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_recharts.ResponsiveContainer, { width: "100%", height: "100%", children })
|
|
347
|
+
}
|
|
348
|
+
) })
|
|
349
|
+
);
|
|
350
|
+
ChartContainer.displayName = "ChartContainer";
|
|
351
|
+
var ChartTooltip = import_recharts.Tooltip;
|
|
352
|
+
var ChartTooltipContent = React.forwardRef(
|
|
353
|
+
({
|
|
354
|
+
active,
|
|
355
|
+
payload,
|
|
356
|
+
label,
|
|
357
|
+
className,
|
|
358
|
+
indicator = "dot",
|
|
359
|
+
labelFormatter,
|
|
360
|
+
valueFormatter,
|
|
361
|
+
...props
|
|
362
|
+
}, ref) => {
|
|
363
|
+
const { config } = useChart();
|
|
364
|
+
if (!active || !payload?.length) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
368
|
+
"div",
|
|
369
|
+
{
|
|
370
|
+
ref,
|
|
371
|
+
className: cn(className),
|
|
372
|
+
style: {
|
|
373
|
+
border: "1px solid rgba(15, 23, 42, 0.15)",
|
|
374
|
+
borderRadius: 12,
|
|
375
|
+
background: "rgba(255,255,255,0.96)",
|
|
376
|
+
boxShadow: "0 12px 40px rgba(15, 23, 42, 0.12)",
|
|
377
|
+
padding: "0.75rem",
|
|
378
|
+
minWidth: 180
|
|
379
|
+
},
|
|
380
|
+
...props,
|
|
381
|
+
children: [
|
|
382
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 8 }, children: labelFormatter ? labelFormatter(label ?? "") : label }),
|
|
383
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "grid", gap: 6 }, children: payload.map((item) => {
|
|
384
|
+
const key = String(item.dataKey ?? item.name ?? "");
|
|
385
|
+
const entry = config[key];
|
|
386
|
+
const color = item.color ?? entry?.color ?? "#0f172a";
|
|
387
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
388
|
+
"div",
|
|
389
|
+
{
|
|
390
|
+
style: {
|
|
391
|
+
display: "flex",
|
|
392
|
+
alignItems: "center",
|
|
393
|
+
justifyContent: "space-between",
|
|
394
|
+
gap: 12,
|
|
395
|
+
fontSize: 12
|
|
396
|
+
},
|
|
397
|
+
children: [
|
|
398
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
|
|
399
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
400
|
+
"span",
|
|
401
|
+
{
|
|
402
|
+
"aria-hidden": "true",
|
|
403
|
+
style: {
|
|
404
|
+
width: indicator === "dot" ? 8 : 12,
|
|
405
|
+
height: 8,
|
|
406
|
+
borderRadius: indicator === "dot" ? 999 : 2,
|
|
407
|
+
background: color,
|
|
408
|
+
display: "inline-block"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: entry?.label ?? item.name })
|
|
413
|
+
] }),
|
|
414
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 600 }, children: valueFormatter ? valueFormatter(item.value ?? "", item.name ?? "") : item.value })
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
key
|
|
418
|
+
);
|
|
419
|
+
}) })
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
ChartTooltipContent.displayName = "ChartTooltipContent";
|
|
426
|
+
|
|
427
|
+
// src/components/LactateCurveChart.tsx
|
|
428
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
429
|
+
var mergeTheme = (theme) => ({
|
|
430
|
+
...DEFAULT_THEME,
|
|
431
|
+
...theme
|
|
432
|
+
});
|
|
433
|
+
var formatNumber = (value, precision) => value.toFixed(Math.max(0, precision));
|
|
434
|
+
var resolveCompressionEnd = (compression, values) => {
|
|
435
|
+
if (!compression) return null;
|
|
436
|
+
const end = compression.endIntensity;
|
|
437
|
+
if (typeof end === "number") return end;
|
|
438
|
+
if (end === "lt1") return values.lt1;
|
|
439
|
+
if (end === "lt2") return values.lt2;
|
|
440
|
+
if (end === "vo2max") return values.vo2max ?? null;
|
|
441
|
+
return null;
|
|
442
|
+
};
|
|
443
|
+
var defaultThresholdLabel = (key) => {
|
|
444
|
+
switch (key) {
|
|
445
|
+
case "lt1":
|
|
446
|
+
return "LT1";
|
|
447
|
+
case "lt2":
|
|
448
|
+
return "LT2";
|
|
449
|
+
case "vo2max":
|
|
450
|
+
return "VO2max";
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var LactateCurveChart = ({
|
|
454
|
+
baselineLactate,
|
|
455
|
+
lt1,
|
|
456
|
+
lt2,
|
|
457
|
+
vo2max,
|
|
458
|
+
zones,
|
|
459
|
+
raceMarkers,
|
|
460
|
+
xAxisLabel = "Intensity",
|
|
461
|
+
yAxisLabel = "Blood lactate (mmol/L)",
|
|
462
|
+
xAxisPrecision = 1,
|
|
463
|
+
yAxisPrecision = 1,
|
|
464
|
+
showXAxisTicks = true,
|
|
465
|
+
xAxisCompression,
|
|
466
|
+
title = "Approximate lactate curve",
|
|
467
|
+
description = "A physiologically plausible lactate curve with thresholds, optional domains, and optional race markers.",
|
|
468
|
+
showThresholdLabels = true,
|
|
469
|
+
showThresholdGuides = true,
|
|
470
|
+
showThresholdCircles = true,
|
|
471
|
+
showGrid = true,
|
|
472
|
+
showLegend = true,
|
|
473
|
+
thresholdAnnotations,
|
|
474
|
+
theme,
|
|
475
|
+
icon,
|
|
476
|
+
labels,
|
|
477
|
+
minIntensity,
|
|
478
|
+
xAxisMax,
|
|
479
|
+
maxIntensity,
|
|
480
|
+
intensityStep,
|
|
481
|
+
curveSamples = 96,
|
|
482
|
+
height = 420,
|
|
483
|
+
style,
|
|
484
|
+
className
|
|
485
|
+
}) => {
|
|
486
|
+
const resolvedTheme = React2.useMemo(() => mergeTheme(theme), [theme]);
|
|
487
|
+
const curve = React2.useMemo(
|
|
488
|
+
() => generateLactateCurve({
|
|
489
|
+
baselineLactate,
|
|
490
|
+
lt1,
|
|
491
|
+
lt2,
|
|
492
|
+
vo2max,
|
|
493
|
+
minIntensity,
|
|
494
|
+
maxIntensity: xAxisMax ?? maxIntensity,
|
|
495
|
+
intensityStep,
|
|
496
|
+
samples: curveSamples
|
|
497
|
+
}),
|
|
498
|
+
[
|
|
499
|
+
baselineLactate,
|
|
500
|
+
curveSamples,
|
|
501
|
+
intensityStep,
|
|
502
|
+
lt1,
|
|
503
|
+
lt2,
|
|
504
|
+
maxIntensity,
|
|
505
|
+
minIntensity,
|
|
506
|
+
vo2max,
|
|
507
|
+
xAxisMax
|
|
508
|
+
]
|
|
509
|
+
);
|
|
510
|
+
const resolvedZones = React2.useMemo(
|
|
511
|
+
() => zones === null ? [] : resolveZones({ zones, curve }),
|
|
512
|
+
[curve, zones]
|
|
513
|
+
);
|
|
514
|
+
const compression = React2.useMemo(() => {
|
|
515
|
+
const end = resolveCompressionEnd(xAxisCompression, {
|
|
516
|
+
lt1: lt1.intensity,
|
|
517
|
+
lt2: lt2.intensity,
|
|
518
|
+
vo2max: vo2max?.intensity
|
|
519
|
+
});
|
|
520
|
+
const scale = xAxisCompression?.scale ?? 0.42;
|
|
521
|
+
if (!end || end <= curve.domain.minIntensity || end >= curve.domain.maxIntensity) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
if (!(scale > 0 && scale < 1)) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const transform = (value) => {
|
|
528
|
+
if (value <= end) {
|
|
529
|
+
return curve.domain.minIntensity + (value - curve.domain.minIntensity) * scale;
|
|
530
|
+
}
|
|
531
|
+
return curve.domain.minIntensity + (end - curve.domain.minIntensity) * scale + (value - end);
|
|
532
|
+
};
|
|
533
|
+
const inverse = (value) => {
|
|
534
|
+
const compressedEnd = curve.domain.minIntensity + (end - curve.domain.minIntensity) * scale;
|
|
535
|
+
if (value <= compressedEnd) {
|
|
536
|
+
return curve.domain.minIntensity + (value - curve.domain.minIntensity) / scale;
|
|
537
|
+
}
|
|
538
|
+
return end + (value - compressedEnd);
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
end,
|
|
542
|
+
scale,
|
|
543
|
+
transform,
|
|
544
|
+
inverse
|
|
545
|
+
};
|
|
546
|
+
}, [
|
|
547
|
+
curve.domain.maxIntensity,
|
|
548
|
+
curve.domain.minIntensity,
|
|
549
|
+
lt1.intensity,
|
|
550
|
+
lt2.intensity,
|
|
551
|
+
vo2max?.intensity,
|
|
552
|
+
xAxisCompression
|
|
553
|
+
]);
|
|
554
|
+
const displayPoints = React2.useMemo(
|
|
555
|
+
() => curve.points.map((point) => ({
|
|
556
|
+
...point,
|
|
557
|
+
displayIntensity: compression?.transform(point.intensity) ?? point.intensity
|
|
558
|
+
})),
|
|
559
|
+
[compression, curve.points]
|
|
560
|
+
);
|
|
561
|
+
const projectedMarkers = React2.useMemo(
|
|
562
|
+
() => (raceMarkers ?? []).map((marker) => projectMarkerToCurve(marker, curve.points)),
|
|
563
|
+
[curve.points, raceMarkers]
|
|
564
|
+
);
|
|
565
|
+
const chartConfig = React2.useMemo(
|
|
566
|
+
() => ({
|
|
567
|
+
lactate: {
|
|
568
|
+
label: labels?.curve ?? "Lactate curve",
|
|
569
|
+
color: resolvedTheme.curveColor
|
|
570
|
+
},
|
|
571
|
+
raceMarkers: {
|
|
572
|
+
label: labels?.raceMarkers ?? "Race intensities",
|
|
573
|
+
color: resolvedTheme.curveColor
|
|
574
|
+
}
|
|
575
|
+
}),
|
|
576
|
+
[labels?.curve, labels?.raceMarkers, resolvedTheme.curveColor]
|
|
577
|
+
);
|
|
578
|
+
const thresholdEntries = [
|
|
579
|
+
{ key: "lt1", point: { ...lt1, ...projectIntensityToCurve(lt1.intensity, curve.points) } },
|
|
580
|
+
{ key: "lt2", point: { ...lt2, ...projectIntensityToCurve(lt2.intensity, curve.points) } },
|
|
581
|
+
vo2max ? {
|
|
582
|
+
key: "vo2max",
|
|
583
|
+
point: { ...vo2max, ...projectIntensityToCurve(vo2max.intensity, curve.points) }
|
|
584
|
+
} : null
|
|
585
|
+
].filter(Boolean);
|
|
586
|
+
const displayDomain = React2.useMemo(
|
|
587
|
+
() => ({
|
|
588
|
+
min: compression?.transform(curve.domain.minIntensity) ?? curve.domain.minIntensity,
|
|
589
|
+
max: compression?.transform(curve.domain.maxIntensity) ?? curve.domain.maxIntensity
|
|
590
|
+
}),
|
|
591
|
+
[compression, curve.domain.maxIntensity, curve.domain.minIntensity]
|
|
592
|
+
);
|
|
593
|
+
const toDisplayIntensity = React2.useCallback(
|
|
594
|
+
(value) => compression?.transform(value) ?? value,
|
|
595
|
+
[compression]
|
|
596
|
+
);
|
|
597
|
+
const iconNode = typeof icon === "function" ? icon({
|
|
598
|
+
width: 18,
|
|
599
|
+
height: 18,
|
|
600
|
+
viewBox: "0 0 18 18"
|
|
601
|
+
}) : icon;
|
|
602
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
603
|
+
"figure",
|
|
604
|
+
{
|
|
605
|
+
className: cn(className),
|
|
606
|
+
style: {
|
|
607
|
+
margin: 0,
|
|
608
|
+
color: resolvedTheme.labelColor,
|
|
609
|
+
fontFamily: resolvedTheme.fontFamily,
|
|
610
|
+
...style
|
|
611
|
+
},
|
|
612
|
+
children: [
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
614
|
+
"div",
|
|
615
|
+
{
|
|
616
|
+
style: {
|
|
617
|
+
display: "flex",
|
|
618
|
+
alignItems: "center",
|
|
619
|
+
justifyContent: "space-between",
|
|
620
|
+
gap: 16,
|
|
621
|
+
marginBottom: 16
|
|
622
|
+
},
|
|
623
|
+
children: [
|
|
624
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("figcaption", { children: [
|
|
625
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 18, fontWeight: 700 }, children: title }),
|
|
626
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 14, opacity: 0.78, marginTop: 4 }, children: description })
|
|
627
|
+
] }),
|
|
628
|
+
iconNode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "aria-hidden": "true", children: iconNode }) : null
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
),
|
|
632
|
+
showLegend ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
633
|
+
"div",
|
|
634
|
+
{
|
|
635
|
+
"aria-label": "Chart legend",
|
|
636
|
+
style: {
|
|
637
|
+
display: "flex",
|
|
638
|
+
alignItems: "center",
|
|
639
|
+
gap: 14,
|
|
640
|
+
flexWrap: "wrap",
|
|
641
|
+
marginBottom: 12,
|
|
642
|
+
fontSize: 12,
|
|
643
|
+
color: resolvedTheme.labelColor
|
|
644
|
+
},
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
|
|
647
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
648
|
+
"span",
|
|
649
|
+
{
|
|
650
|
+
"aria-hidden": "true",
|
|
651
|
+
style: {
|
|
652
|
+
width: 24,
|
|
653
|
+
height: 0,
|
|
654
|
+
borderTop: `3px solid ${resolvedTheme.curveColor}`,
|
|
655
|
+
borderRadius: 999,
|
|
656
|
+
display: "inline-block"
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
),
|
|
660
|
+
chartConfig.lactate.label
|
|
661
|
+
] }),
|
|
662
|
+
projectedMarkers.length ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [
|
|
663
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
664
|
+
"span",
|
|
665
|
+
{
|
|
666
|
+
"aria-hidden": "true",
|
|
667
|
+
style: {
|
|
668
|
+
width: 10,
|
|
669
|
+
height: 10,
|
|
670
|
+
borderRadius: 999,
|
|
671
|
+
background: resolvedTheme.curveColor,
|
|
672
|
+
display: "inline-block"
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
),
|
|
676
|
+
chartConfig.raceMarkers.label
|
|
677
|
+
] }) : null
|
|
678
|
+
]
|
|
679
|
+
}
|
|
680
|
+
) : null,
|
|
681
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
682
|
+
ChartContainer,
|
|
683
|
+
{
|
|
684
|
+
config: chartConfig,
|
|
685
|
+
height,
|
|
686
|
+
className: "lactate-curve-chart",
|
|
687
|
+
role: "img",
|
|
688
|
+
"aria-label": title,
|
|
689
|
+
style: {
|
|
690
|
+
background: resolvedTheme.backgroundColor,
|
|
691
|
+
border: "1px solid rgba(148, 163, 184, 0.22)",
|
|
692
|
+
borderRadius: 20,
|
|
693
|
+
padding: 12
|
|
694
|
+
},
|
|
695
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
696
|
+
import_recharts2.LineChart,
|
|
697
|
+
{
|
|
698
|
+
data: displayPoints,
|
|
699
|
+
margin: { top: 24, right: 28, bottom: 28, left: 14 },
|
|
700
|
+
children: [
|
|
701
|
+
showGrid ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
702
|
+
import_recharts2.CartesianGrid,
|
|
703
|
+
{
|
|
704
|
+
stroke: resolvedTheme.gridColor,
|
|
705
|
+
strokeDasharray: "3 3",
|
|
706
|
+
vertical: false
|
|
707
|
+
}
|
|
708
|
+
) : null,
|
|
709
|
+
resolvedZones.map((zone) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
710
|
+
import_recharts2.ReferenceArea,
|
|
711
|
+
{
|
|
712
|
+
x1: toDisplayIntensity(zone.startIntensity),
|
|
713
|
+
x2: toDisplayIntensity(zone.endIntensity),
|
|
714
|
+
fill: zone.color,
|
|
715
|
+
fillOpacity: zone.opacity,
|
|
716
|
+
strokeOpacity: 0,
|
|
717
|
+
ifOverflow: "visible"
|
|
718
|
+
},
|
|
719
|
+
`${zone.label}-${zone.startIntensity}-${zone.endIntensity}`
|
|
720
|
+
)),
|
|
721
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
722
|
+
import_recharts2.XAxis,
|
|
723
|
+
{
|
|
724
|
+
type: "number",
|
|
725
|
+
dataKey: "displayIntensity",
|
|
726
|
+
domain: [displayDomain.min, displayDomain.max],
|
|
727
|
+
tick: showXAxisTicks ? { fill: resolvedTheme.axisColor, fontFamily: resolvedTheme.fontFamily } : false,
|
|
728
|
+
axisLine: { stroke: resolvedTheme.axisColor },
|
|
729
|
+
tickLine: showXAxisTicks ? { stroke: resolvedTheme.axisColor } : false,
|
|
730
|
+
tickFormatter: (value) => formatNumber(
|
|
731
|
+
compression?.inverse(Number(value)) ?? Number(value),
|
|
732
|
+
xAxisPrecision
|
|
733
|
+
),
|
|
734
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
735
|
+
import_recharts2.Label,
|
|
736
|
+
{
|
|
737
|
+
value: xAxisLabel,
|
|
738
|
+
position: "insideBottom",
|
|
739
|
+
offset: -14,
|
|
740
|
+
style: {
|
|
741
|
+
fill: resolvedTheme.labelColor,
|
|
742
|
+
fontFamily: resolvedTheme.fontFamily
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
749
|
+
import_recharts2.YAxis,
|
|
750
|
+
{
|
|
751
|
+
type: "number",
|
|
752
|
+
domain: [curve.domain.minLactate, curve.domain.maxLactate],
|
|
753
|
+
tick: { fill: resolvedTheme.axisColor, fontFamily: resolvedTheme.fontFamily },
|
|
754
|
+
axisLine: { stroke: resolvedTheme.axisColor },
|
|
755
|
+
tickLine: { stroke: resolvedTheme.axisColor },
|
|
756
|
+
tickFormatter: (value) => formatNumber(Number(value), yAxisPrecision),
|
|
757
|
+
width: 64,
|
|
758
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
759
|
+
import_recharts2.Label,
|
|
760
|
+
{
|
|
761
|
+
angle: -90,
|
|
762
|
+
position: "insideLeft",
|
|
763
|
+
value: yAxisLabel,
|
|
764
|
+
style: {
|
|
765
|
+
fill: resolvedTheme.labelColor,
|
|
766
|
+
fontFamily: resolvedTheme.fontFamily,
|
|
767
|
+
textAnchor: "middle"
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
),
|
|
773
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
774
|
+
ChartTooltip,
|
|
775
|
+
{
|
|
776
|
+
cursor: { stroke: resolvedTheme.gridColor, strokeDasharray: "3 3" },
|
|
777
|
+
content: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
778
|
+
ChartTooltipContent,
|
|
779
|
+
{
|
|
780
|
+
labelFormatter: (value) => `${xAxisLabel}: ${formatNumber(
|
|
781
|
+
compression?.inverse(Number(value)) ?? Number(value),
|
|
782
|
+
xAxisPrecision
|
|
783
|
+
)}`,
|
|
784
|
+
valueFormatter: (value, name) => name === "lactate" ? `${formatNumber(Number(value), yAxisPrecision)} mmol/L` : `${formatNumber(Number(value), xAxisPrecision)}`
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
),
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
790
|
+
import_recharts2.Line,
|
|
791
|
+
{
|
|
792
|
+
type: "monotone",
|
|
793
|
+
dataKey: "lactate",
|
|
794
|
+
name: "lactate",
|
|
795
|
+
stroke: resolvedTheme.curveColor,
|
|
796
|
+
strokeWidth: 3,
|
|
797
|
+
dot: false,
|
|
798
|
+
isAnimationActive: false
|
|
799
|
+
}
|
|
800
|
+
),
|
|
801
|
+
thresholdEntries.map(({ key, point }) => {
|
|
802
|
+
const annotation = thresholdAnnotations?.[key];
|
|
803
|
+
const color = annotation?.color ?? resolvedTheme.thresholdColor;
|
|
804
|
+
const showGuide = annotation?.showGuide ?? showThresholdGuides;
|
|
805
|
+
const showCircle = annotation?.showCircle ?? showThresholdCircles;
|
|
806
|
+
const labelText = annotation?.label ?? labels?.[key] ?? defaultThresholdLabel(key);
|
|
807
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(React2.Fragment, { children: [
|
|
808
|
+
showGuide ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
809
|
+
import_recharts2.ReferenceLine,
|
|
810
|
+
{
|
|
811
|
+
x: toDisplayIntensity(point.intensity),
|
|
812
|
+
stroke: color,
|
|
813
|
+
strokeDasharray: annotation?.strokeDasharray ?? "6 4",
|
|
814
|
+
strokeWidth: annotation?.lineWidth ?? 1.5,
|
|
815
|
+
ifOverflow: "visible",
|
|
816
|
+
label: showThresholdLabels ? {
|
|
817
|
+
value: labelText,
|
|
818
|
+
position: "top",
|
|
819
|
+
fill: color,
|
|
820
|
+
fontSize: 12,
|
|
821
|
+
fontWeight: 700,
|
|
822
|
+
fontFamily: resolvedTheme.fontFamily
|
|
823
|
+
} : void 0
|
|
824
|
+
}
|
|
825
|
+
) : null,
|
|
826
|
+
showCircle ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
827
|
+
import_recharts2.ReferenceDot,
|
|
828
|
+
{
|
|
829
|
+
x: toDisplayIntensity(point.intensity),
|
|
830
|
+
y: point.lactate,
|
|
831
|
+
r: annotation?.circleRadius ?? 5,
|
|
832
|
+
fill: color,
|
|
833
|
+
stroke: resolvedTheme.backgroundColor,
|
|
834
|
+
strokeWidth: 2,
|
|
835
|
+
ifOverflow: "visible"
|
|
836
|
+
}
|
|
837
|
+
) : null
|
|
838
|
+
] }, key);
|
|
839
|
+
}),
|
|
840
|
+
projectedMarkers.map((marker) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
841
|
+
import_recharts2.ReferenceDot,
|
|
842
|
+
{
|
|
843
|
+
x: toDisplayIntensity(marker.intensity),
|
|
844
|
+
y: marker.lactate,
|
|
845
|
+
r: marker.radius ?? 5,
|
|
846
|
+
fill: marker.color ?? resolvedTheme.curveColor,
|
|
847
|
+
stroke: resolvedTheme.backgroundColor,
|
|
848
|
+
strokeWidth: 2,
|
|
849
|
+
ifOverflow: "visible",
|
|
850
|
+
label: marker.showLabel === false ? void 0 : {
|
|
851
|
+
value: marker.label,
|
|
852
|
+
position: "top",
|
|
853
|
+
offset: 10,
|
|
854
|
+
fill: resolvedTheme.markerLabelColor,
|
|
855
|
+
fontSize: 12,
|
|
856
|
+
fontWeight: 600,
|
|
857
|
+
fontFamily: resolvedTheme.fontFamily
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
`${marker.label}-${marker.intensity}`
|
|
861
|
+
))
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
),
|
|
867
|
+
compression ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
868
|
+
"div",
|
|
869
|
+
{
|
|
870
|
+
style: {
|
|
871
|
+
marginTop: 10,
|
|
872
|
+
fontSize: 12,
|
|
873
|
+
color: resolvedTheme.labelColor,
|
|
874
|
+
opacity: 0.78
|
|
875
|
+
},
|
|
876
|
+
children: [
|
|
877
|
+
"X-axis break: the low-intensity region up to ",
|
|
878
|
+
formatNumber(
|
|
879
|
+
compression.end,
|
|
880
|
+
xAxisPrecision
|
|
881
|
+
),
|
|
882
|
+
" is visually compressed."
|
|
883
|
+
]
|
|
884
|
+
}
|
|
885
|
+
) : null,
|
|
886
|
+
resolvedZones.length ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
887
|
+
"div",
|
|
888
|
+
{
|
|
889
|
+
"aria-label": labels?.zones ?? "Intensity domains",
|
|
890
|
+
style: {
|
|
891
|
+
display: "flex",
|
|
892
|
+
flexWrap: "wrap",
|
|
893
|
+
gap: 10,
|
|
894
|
+
marginTop: 14
|
|
895
|
+
},
|
|
896
|
+
children: resolvedZones.map((zone) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
897
|
+
"span",
|
|
898
|
+
{
|
|
899
|
+
style: {
|
|
900
|
+
display: "inline-flex",
|
|
901
|
+
alignItems: "center",
|
|
902
|
+
gap: 8,
|
|
903
|
+
fontSize: 12,
|
|
904
|
+
color: resolvedTheme.labelColor
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
908
|
+
"span",
|
|
909
|
+
{
|
|
910
|
+
"aria-hidden": "true",
|
|
911
|
+
style: {
|
|
912
|
+
width: 12,
|
|
913
|
+
height: 12,
|
|
914
|
+
borderRadius: 999,
|
|
915
|
+
background: zone.color,
|
|
916
|
+
opacity: Math.max(zone.opacity, 0.35)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
),
|
|
920
|
+
zone.label
|
|
921
|
+
]
|
|
922
|
+
},
|
|
923
|
+
`${zone.label}-${zone.startIntensity}-${zone.endIntensity}-legend`
|
|
924
|
+
))
|
|
925
|
+
}
|
|
926
|
+
) : null
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// src/examples.tsx
|
|
933
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
934
|
+
var baseProps = {
|
|
935
|
+
baselineLactate: 1.1,
|
|
936
|
+
lt1: { intensity: 220, lactate: 1.9 },
|
|
937
|
+
lt2: { intensity: 305, lactate: 4.1 }
|
|
938
|
+
};
|
|
939
|
+
var BasicCurveExample = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LactateCurveChart, { ...baseProps });
|
|
940
|
+
var CurveWithVo2maxExample = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
941
|
+
LactateCurveChart,
|
|
942
|
+
{
|
|
943
|
+
...baseProps,
|
|
944
|
+
vo2max: { intensity: 360 },
|
|
945
|
+
description: "The severe-domain tail is anchored explicitly with a VO2max point.",
|
|
946
|
+
xAxisCompression: { endIntensity: "lt1", scale: 0.34 }
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
var DomainsOverlayExample = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
950
|
+
LactateCurveChart,
|
|
951
|
+
{
|
|
952
|
+
...baseProps,
|
|
953
|
+
vo2max: { intensity: 355 },
|
|
954
|
+
xAxisLabel: "Running power (W)"
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
var RaceMarkerOverlayExample = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
958
|
+
LactateCurveChart,
|
|
959
|
+
{
|
|
960
|
+
...baseProps,
|
|
961
|
+
vo2max: { intensity: 360 },
|
|
962
|
+
raceMarkers: [
|
|
963
|
+
{ intensity: 252, label: "Marathon", color: "#0f766e" },
|
|
964
|
+
{ intensity: 300, label: "Half", color: "#0369a1" },
|
|
965
|
+
{ intensity: 312, label: "10k", color: "#7c3aed" },
|
|
966
|
+
{ intensity: 329, label: "5k", color: "#dc2626" },
|
|
967
|
+
{ intensity: 342, label: "3k", color: "#ea580c" }
|
|
968
|
+
]
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
var BrandMark = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "22", height: "22", viewBox: "0 0 22 22", fill: "none", children: [
|
|
972
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "11", cy: "11", r: "10", fill: "#102a43" }),
|
|
973
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
974
|
+
"path",
|
|
975
|
+
{
|
|
976
|
+
d: "M6 13.8C8.7 13.8 10.8 12 11.8 8.8C12.7 12.7 14.8 15 17 15",
|
|
977
|
+
stroke: "#f0b429",
|
|
978
|
+
strokeWidth: "2.2",
|
|
979
|
+
strokeLinecap: "round",
|
|
980
|
+
strokeLinejoin: "round"
|
|
981
|
+
}
|
|
982
|
+
)
|
|
983
|
+
] });
|
|
984
|
+
var FullyThemedExample = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
985
|
+
LactateCurveChart,
|
|
986
|
+
{
|
|
987
|
+
...baseProps,
|
|
988
|
+
vo2max: { intensity: 362 },
|
|
989
|
+
title: "Threshold Map",
|
|
990
|
+
description: "A branded version for article embeds and product UI.",
|
|
991
|
+
xAxisLabel: "Power (W)",
|
|
992
|
+
yAxisLabel: "Lactate (mmol/L)",
|
|
993
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BrandMark, {}),
|
|
994
|
+
labels: {
|
|
995
|
+
curve: "Athlete curve",
|
|
996
|
+
zones: "Training domains",
|
|
997
|
+
lt1: "Aerobic threshold",
|
|
998
|
+
lt2: "Critical threshold",
|
|
999
|
+
vo2max: "VO2 ceiling"
|
|
1000
|
+
},
|
|
1001
|
+
raceMarkers: [
|
|
1002
|
+
{ intensity: 255, label: "M", color: "#166534", radius: 6 },
|
|
1003
|
+
{ intensity: 298, label: "HM", color: "#0f766e", radius: 6 },
|
|
1004
|
+
{ intensity: 313, label: "10k", color: "#1d4ed8", radius: 6 },
|
|
1005
|
+
{ intensity: 332, label: "5k", color: "#b91c1c", radius: 6 }
|
|
1006
|
+
],
|
|
1007
|
+
theme: {
|
|
1008
|
+
curveColor: "#b91c1c",
|
|
1009
|
+
thresholdColor: "#102a43",
|
|
1010
|
+
axisColor: "#243b53",
|
|
1011
|
+
gridColor: "#d9e2ec",
|
|
1012
|
+
labelColor: "#102a43",
|
|
1013
|
+
markerLabelColor: "#102a43",
|
|
1014
|
+
backgroundColor: "#f8fbff",
|
|
1015
|
+
fontFamily: "Avenir Next, ui-sans-serif, system-ui, sans-serif"
|
|
1016
|
+
},
|
|
1017
|
+
zones: [
|
|
1018
|
+
{
|
|
1019
|
+
startIntensity: "min",
|
|
1020
|
+
endIntensity: "lt1",
|
|
1021
|
+
label: "Easy / moderate",
|
|
1022
|
+
color: "#a7f3d0"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
startIntensity: "lt1",
|
|
1026
|
+
endIntensity: "lt2",
|
|
1027
|
+
label: "Threshold / heavy",
|
|
1028
|
+
color: "#fde68a"
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
startIntensity: "lt2",
|
|
1032
|
+
endIntensity: "max",
|
|
1033
|
+
label: "VO2 / severe",
|
|
1034
|
+
color: "#fecaca"
|
|
1035
|
+
}
|
|
1036
|
+
]
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1040
|
+
0 && (module.exports = {
|
|
1041
|
+
BasicCurveExample,
|
|
1042
|
+
CurveWithVo2maxExample,
|
|
1043
|
+
DomainsOverlayExample,
|
|
1044
|
+
FullyThemedExample,
|
|
1045
|
+
LactateCurveChart,
|
|
1046
|
+
RaceMarkerOverlayExample,
|
|
1047
|
+
generateLactateCurve,
|
|
1048
|
+
projectMarkerToCurve,
|
|
1049
|
+
resolveZones
|
|
1050
|
+
});
|
|
1051
|
+
//# sourceMappingURL=index.cjs.map
|