diff --git a/public/data/calculators/bmi-lms.json b/public/data/calculators/bmi-lms.json new file mode 100644 index 0000000..dba58cf --- /dev/null +++ b/public/data/calculators/bmi-lms.json @@ -0,0 +1,380 @@ +{ + "version": "1.0", + "source": "CDC 2000 BMI-for-age LMS values, 24-240 months every 6 months as previously embedded in calculators.js", + "lms": { + "male": { + "24": { + "L": -1.982374, + "M": 16.5478, + "S": 0.080127 + }, + "30": { + "L": -1.642107, + "M": 16.2497, + "S": 0.075499 + }, + "36": { + "L": -1.419991, + "M": 16.0003, + "S": 0.072634 + }, + "42": { + "L": -1.438165, + "M": 15.7941, + "S": 0.071495 + }, + "48": { + "L": -1.714869, + "M": 15.6282, + "S": 0.071889 + }, + "54": { + "L": -2.155348, + "M": 15.5026, + "S": 0.073491 + }, + "60": { + "L": -2.615166, + "M": 15.4191, + "S": 0.075992 + }, + "66": { + "L": -2.981797, + "M": 15.3795, + "S": 0.079211 + }, + "72": { + "L": -3.211705, + "M": 15.3835, + "S": 0.083048 + }, + "78": { + "L": -3.314769, + "M": 15.429, + "S": 0.0874 + }, + "84": { + "L": -3.323189, + "M": 15.5129, + "S": 0.092131 + }, + "90": { + "L": -3.270455, + "M": 15.6317, + "S": 0.097082 + }, + "96": { + "L": -3.183058, + "M": 15.7823, + "S": 0.102091 + }, + "102": { + "L": -3.079383, + "M": 15.9617, + "S": 0.107013 + }, + "108": { + "L": -2.971148, + "M": 16.1671, + "S": 0.111721 + }, + "114": { + "L": -2.865311, + "M": 16.3961, + "S": 0.116113 + }, + "120": { + "L": -2.765648, + "M": 16.6461, + "S": 0.120112 + }, + "126": { + "L": -2.673903, + "M": 16.9151, + "S": 0.123664 + }, + "132": { + "L": -2.59056, + "M": 17.2009, + "S": 0.126735 + }, + "138": { + "L": -2.51532, + "M": 17.5014, + "S": 0.129309 + }, + "144": { + "L": -2.447426, + "M": 17.8146, + "S": 0.131389 + }, + "150": { + "L": -2.385858, + "M": 18.1387, + "S": 0.132991 + }, + "156": { + "L": -2.329457, + "M": 18.4718, + "S": 0.134141 + }, + "162": { + "L": -2.277017, + "M": 18.812, + "S": 0.13488 + }, + "168": { + "L": -2.227362, + "M": 19.1576, + "S": 0.135251 + }, + "174": { + "L": -2.179426, + "M": 19.5067, + "S": 0.135309 + }, + "180": { + "L": -2.132345, + "M": 19.8577, + "S": 0.13511 + }, + "186": { + "L": -2.085574, + "M": 20.2086, + "S": 0.134718 + }, + "192": { + "L": -2.039015, + "M": 20.5576, + "S": 0.134198 + }, + "198": { + "L": -1.99315, + "M": 20.9029, + "S": 0.13362 + }, + "204": { + "L": -1.949135, + "M": 21.2425, + "S": 0.133057 + }, + "210": { + "L": -1.908831, + "M": 21.5742, + "S": 0.132585 + }, + "216": { + "L": -1.87467, + "M": 21.8959, + "S": 0.132286 + }, + "222": { + "L": -1.849323, + "M": 22.2054, + "S": 0.132249 + }, + "228": { + "L": -1.835138, + "M": 22.5007, + "S": 0.132566 + }, + "234": { + "L": -1.833401, + "M": 22.7799, + "S": 0.133339 + }, + "240": { + "L": -1.843581, + "M": 23.0414, + "S": 0.134675 + } + }, + "female": { + "24": { + "L": -1.024497, + "M": 16.388, + "S": 0.085026 + }, + "30": { + "L": -1.534542, + "M": 16.0059, + "S": 0.080932 + }, + "36": { + "L": -2.096829, + "M": 15.6992, + "S": 0.078605 + }, + "42": { + "L": -2.618733, + "M": 15.4647, + "S": 0.077904 + }, + "48": { + "L": -3.018522, + "M": 15.2985, + "S": 0.078713 + }, + "54": { + "L": -3.2593, + "M": 15.1961, + "S": 0.080904 + }, + "60": { + "L": -3.350078, + "M": 15.1519, + "S": 0.0843 + }, + "66": { + "L": -3.325522, + "M": 15.1606, + "S": 0.08868 + }, + "72": { + "L": -3.225607, + "M": 15.2169, + "S": 0.093803 + }, + "78": { + "L": -3.084291, + "M": 15.3161, + "S": 0.099427 + }, + "84": { + "L": -2.926187, + "M": 15.4536, + "S": 0.105325 + }, + "90": { + "L": -2.76731, + "M": 15.6252, + "S": 0.111295 + }, + "96": { + "L": -2.617192, + "M": 15.827, + "S": 0.117159 + }, + "102": { + "L": -2.480952, + "M": 16.0552, + "S": 0.122771 + }, + "108": { + "L": -2.360921, + "M": 16.3061, + "S": 0.128014 + }, + "114": { + "L": -2.257782, + "M": 16.5763, + "S": 0.132797 + }, + "120": { + "L": -2.171296, + "M": 16.8623, + "S": 0.137057 + }, + "126": { + "L": -2.100749, + "M": 17.161, + "S": 0.140754 + }, + "132": { + "L": -2.045235, + "M": 17.4691, + "S": 0.143868 + }, + "138": { + "L": -2.003802, + "M": 17.7836, + "S": 0.146399 + }, + "144": { + "L": -1.975521, + "M": 18.1015, + "S": 0.148361 + }, + "150": { + "L": -1.95952, + "M": 18.42, + "S": 0.149783 + }, + "156": { + "L": -1.954978, + "M": 18.7364, + "S": 0.150705 + }, + "162": { + "L": -1.9611, + "M": 19.0481, + "S": 0.151176 + }, + "168": { + "L": -1.977074, + "M": 19.3526, + "S": 0.151256 + }, + "174": { + "L": -2.002014, + "M": 19.6475, + "S": 0.15101 + }, + "180": { + "L": -2.034893, + "M": 19.9306, + "S": 0.150512 + }, + "186": { + "L": -2.07446, + "M": 20.1998, + "S": 0.149843 + }, + "192": { + "L": -2.119157, + "M": 20.4533, + "S": 0.14909 + }, + "198": { + "L": -2.167045, + "M": 20.6891, + "S": 0.148349 + }, + "204": { + "L": -2.215738, + "M": 20.9058, + "S": 0.147723 + }, + "210": { + "L": -2.262382, + "M": 21.1016, + "S": 0.147323 + }, + "216": { + "L": -2.303688, + "M": 21.2753, + "S": 0.147269 + }, + "222": { + "L": -2.336038, + "M": 21.4255, + "S": 0.147689 + }, + "228": { + "L": -2.355678, + "M": 21.5508, + "S": 0.148724 + }, + "234": { + "L": -2.35898, + "M": 21.6501, + "S": 0.150521 + }, + "240": { + "L": -2.342797, + "M": 21.7219, + "S": 0.153241 + } + } + } +} diff --git a/public/js/calculators.js b/public/js/calculators.js index 465cd9a..5ff3d99 100644 --- a/public/js/calculators.js +++ b/public/js/calculators.js @@ -1,5 +1,6 @@ import { parseAgeMonths, formatParsedAge } from './calculators/age.js'; import { calculateAapBilirubinAssessment, classifyBhutaniRisk, loadAapBilirubinThresholds, loadBhutaniZones } from './calculators/bilirubin.js'; +import { calculateBMIAssessment, loadBmiLmsData } from './calculators/bmi.js'; import { loadEquipmentData, renderEquipmentResult } from './calculators/equipment.js'; import { initImageLightbox } from './calculators/lightbox.js'; import { loadResusMedsData, renderResusMeds } from './calculators/resus.js'; @@ -701,21 +702,7 @@ initImageLightbox(); // BMI PERCENTILE — CDC 2000 // ═══════════════════════════════════════════════════════════ - // CDC BMI-for-Age 24-240mo (every 3 months, from CDC bmiagerev.csv) - var bmiLMS = {male:{24:{L:-1.982374,M:16.5478,S:0.080127},30:{L:-1.642107,M:16.2497,S:0.075499},36:{L:-1.419991,M:16.0003,S:0.072634},42:{L:-1.438165,M:15.7941,S:0.071495},48:{L:-1.714869,M:15.6282,S:0.071889},54:{L:-2.155348,M:15.5026,S:0.073491},60:{L:-2.615166,M:15.4191,S:0.075992},66:{L:-2.981797,M:15.3795,S:0.079211},72:{L:-3.211705,M:15.3835,S:0.083048},78:{L:-3.314769,M:15.429,S:0.0874},84:{L:-3.323189,M:15.5129,S:0.092131},90:{L:-3.270455,M:15.6317,S:0.097082},96:{L:-3.183058,M:15.7823,S:0.102091},102:{L:-3.079383,M:15.9617,S:0.107013},108:{L:-2.971148,M:16.1671,S:0.111721},114:{L:-2.865311,M:16.3961,S:0.116113},120:{L:-2.765648,M:16.6461,S:0.120112},126:{L:-2.673903,M:16.9151,S:0.123664},132:{L:-2.59056,M:17.2009,S:0.126735},138:{L:-2.51532,M:17.5014,S:0.129309},144:{L:-2.447426,M:17.8146,S:0.131389},150:{L:-2.385858,M:18.1387,S:0.132991},156:{L:-2.329457,M:18.4718,S:0.134141},162:{L:-2.277017,M:18.812,S:0.13488},168:{L:-2.227362,M:19.1576,S:0.135251},174:{L:-2.179426,M:19.5067,S:0.135309},180:{L:-2.132345,M:19.8577,S:0.13511},186:{L:-2.085574,M:20.2086,S:0.134718},192:{L:-2.039015,M:20.5576,S:0.134198},198:{L:-1.99315,M:20.9029,S:0.13362},204:{L:-1.949135,M:21.2425,S:0.133057},210:{L:-1.908831,M:21.5742,S:0.132585},216:{L:-1.87467,M:21.8959,S:0.132286},222:{L:-1.849323,M:22.2054,S:0.132249},228:{L:-1.835138,M:22.5007,S:0.132566},234:{L:-1.833401,M:22.7799,S:0.133339},240:{L:-1.843581,M:23.0414,S:0.134675}},female:{24:{L:-1.024497,M:16.388,S:0.085026},30:{L:-1.534542,M:16.0059,S:0.080932},36:{L:-2.096829,M:15.6992,S:0.078605},42:{L:-2.618733,M:15.4647,S:0.077904},48:{L:-3.018522,M:15.2985,S:0.078713},54:{L:-3.2593,M:15.1961,S:0.080904},60:{L:-3.350078,M:15.1519,S:0.0843},66:{L:-3.325522,M:15.1606,S:0.08868},72:{L:-3.225607,M:15.2169,S:0.093803},78:{L:-3.084291,M:15.3161,S:0.099427},84:{L:-2.926187,M:15.4536,S:0.105325},90:{L:-2.76731,M:15.6252,S:0.111295},96:{L:-2.617192,M:15.827,S:0.117159},102:{L:-2.480952,M:16.0552,S:0.122771},108:{L:-2.360921,M:16.3061,S:0.128014},114:{L:-2.257782,M:16.5763,S:0.132797},120:{L:-2.171296,M:16.8623,S:0.137057},126:{L:-2.100749,M:17.161,S:0.140754},132:{L:-2.045235,M:17.4691,S:0.143868},138:{L:-2.003802,M:17.7836,S:0.146399},144:{L:-1.975521,M:18.1015,S:0.148361},150:{L:-1.95952,M:18.42,S:0.149783},156:{L:-1.954978,M:18.7364,S:0.150705},162:{L:-1.9611,M:19.0481,S:0.151176},168:{L:-1.977074,M:19.3526,S:0.151256},174:{L:-2.002014,M:19.6475,S:0.15101},180:{L:-2.034893,M:19.9306,S:0.150512},186:{L:-2.07446,M:20.1998,S:0.149843},192:{L:-2.119157,M:20.4533,S:0.14909},198:{L:-2.167045,M:20.6891,S:0.148349},204:{L:-2.215738,M:20.9058,S:0.147723},210:{L:-2.262382,M:21.1016,S:0.147323},216:{L:-2.303688,M:21.2753,S:0.147269},222:{L:-2.336038,M:21.4255,S:0.147689},228:{L:-2.355678,M:21.5508,S:0.148724},234:{L:-2.35898,M:21.6501,S:0.150521},240:{L:-2.342797,M:21.7219,S:0.153241}}}; - - function calcBMIPercentile(bmi, L, M, S) { - // Z-score from LMS: Z = ((BMI/M)^L - 1) / (L * S) - var z; - if (Math.abs(L) < 0.001) { - z = Math.log(bmi / M) / S; - } else { - z = (Math.pow(bmi / M, L) - 1) / (L * S); - } - // Convert Z to percentile using standard normal CDF approximation - var p = normalCDF(z); - return { z: z, percentile: Math.round(p * 10000) / 100 }; - } + // CDC BMI-for-age LMS values load from /data/calculators/bmi-lms.json. function normalCDF(z) { var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911; @@ -961,26 +948,10 @@ initImageLightbox(); return { target: mph, low: mph - 8.5, high: mph + 8.5 }; } - // Extended BMI classification per CDC 2022 - // Uses % of 95th percentile for severe obesity staging - function classifyBMI(percentile, bmi, lms) { - // Calculate 95th percentile BMI from LMS - var p95z = 1.645; // Z for 95th percentile - var bmi95; - if (Math.abs(lms.L) < 0.001) bmi95 = lms.M * Math.exp(lms.S * p95z); - else bmi95 = lms.M * Math.pow(1 + lms.L * lms.S * p95z, 1 / lms.L); - - var pctOf95 = (bmi / bmi95) * 100; - - if (percentile >= 95) { - if (pctOf95 >= 140) return { text: 'Class 3 Severe Obesity', color: '#7f1d1d', bg: '#fecaca', pctOf95: pctOf95, bmi95: bmi95 }; - if (pctOf95 >= 120) return { text: 'Class 2 Severe Obesity', color: '#dc2626', bg: '#fee2e2', pctOf95: pctOf95, bmi95: bmi95 }; - return { text: 'Obese (Class 1)', color: '#ef4444', bg: '#fee2e2', pctOf95: pctOf95, bmi95: bmi95 }; - } - if (percentile >= 85) return { text: 'Overweight', color: '#f97316', bg: '#ffedd5', pctOf95: pctOf95, bmi95: bmi95 }; - if (percentile >= 5) return { text: 'Healthy Weight', color: '#10b981', bg: '#d1fae5', pctOf95: pctOf95, bmi95: bmi95 }; - return { text: 'Underweight', color: '#f59e0b', bg: '#fef3c7', pctOf95: pctOf95, bmi95: bmi95 }; - } + var BMI_LMS = null; + loadBmiLmsData() + .then(function(data) { BMI_LMS = data; }) + .catch(function(err) { console.warn('[calculators] BMI LMS data unavailable:', err && err.message); }); document.getElementById('btn-calc-bmi').addEventListener('click', function() { var ageYr = parseFloat(document.getElementById('bmi-age-yr').value) || 0; @@ -992,16 +963,13 @@ initImageLightbox(); if (!age || !sex || !weight || !height) { showToast('Fill in all fields', 'error'); return; } if (age < 2 || age > 20) { showToast('Age must be 2-20 years', 'error'); return; } + if (!BMI_LMS) { showToast('BMI percentile data is still loading.', 'info'); return; } - var bmi = weight / Math.pow(height / 100, 2); - var ageMonths = Math.round(age * 12); - if (ageMonths < 24) ageMonths = 24; - if (ageMonths > 240) ageMonths = 240; - var lms = interpolateLMS(bmiLMS[sex], ageMonths); - if (!lms) { showToast('Invalid age/sex', 'error'); return; } - - var result = calcBMIPercentile(bmi, lms.L, lms.M, lms.S); - var cl = classifyBMI(result.percentile, bmi, lms); + var assessment = calculateBMIAssessment(BMI_LMS, sex, age, weight, height); + var bmi = assessment.bmi; + var ageMonths = assessment.ageMonths; + var result = { percentile: assessment.percentile, z: assessment.z }; + var cl = assessment.classification; var extendedInfo = ''; if (result.percentile >= 85) { @@ -1021,7 +989,7 @@ initImageLightbox(); '
Z-Score
' + result.z.toFixed(2) + '
' + extendedInfo + ''; - var bmiYears = {}; Object.keys(bmiLMS[sex]).forEach(function(k) { bmiYears[+(k/12).toFixed(1)] = bmiLMS[sex][k]; }); + var bmiYears = {}; Object.keys(BMI_LMS[sex]).forEach(function(k) { bmiYears[+(k/12).toFixed(1)] = BMI_LMS[sex][k]; }); renderGrowthChart('bmi-chart-canvas', 'bmi-chart-container', bmiYears, +(ageMonths/12).toFixed(1), bmi, 'Age (years)', 'BMI (kg/m2)'); }); @@ -1335,9 +1303,10 @@ initImageLightbox(); renderGrowthChart('growth-chart-canvas', 'growth-chart-container', wflLMS[sex], len, wt, 'Length (cm)', 'Weight (kg)'); } else { // Fall back to BMI-for-Age for children outside WFL range + if (!BMI_LMS) { showToast('BMI percentile data is still loading.', 'info'); return; } var bmi = wt / Math.pow(len / 100, 2); var ageMonths = Math.max(24, Math.min(240, Math.round(age))); - var lms = interpolateLMS(bmiLMS[sex], ageMonths); + var lms = interpolateLMS(BMI_LMS[sex], ageMonths); var z = calcZFromLMS(bmi, lms.L, lms.M, lms.S); var p = normalCDF(z) * 100; resultDiv.classList.remove('hidden'); diff --git a/public/js/calculators/bmi.js b/public/js/calculators/bmi.js new file mode 100644 index 0000000..b14f337 --- /dev/null +++ b/public/js/calculators/bmi.js @@ -0,0 +1,82 @@ +var bmiCache = null; + +export function loadBmiLmsData() { + if (bmiCache) return Promise.resolve(bmiCache); + return fetch('/data/calculators/bmi-lms.json', { credentials: 'same-origin' }) + .then(function(response) { + if (!response.ok) throw new Error('Failed to load BMI LMS data'); + return response.json(); + }) + .then(function(data) { + bmiCache = data.lms || {}; + return bmiCache; + }); +} + +export function interpolateLMS(lmsTable, val) { + var keys = Object.keys(lmsTable).map(Number).sort(function(a,b){return a-b;}); + if (val <= keys[0]) return lmsTable[keys[0]]; + if (val >= keys[keys.length-1]) return lmsTable[keys[keys.length-1]]; + for (var i = 0; i < keys.length - 1; i++) { + if (val >= keys[i] && val <= keys[i+1]) { + var t = (val - keys[i]) / (keys[i+1] - keys[i]); + var a = lmsTable[keys[i]], b = lmsTable[keys[i+1]]; + return { L: a.L + t*(b.L-a.L), M: a.M + t*(b.M-a.M), S: a.S + t*(b.S-a.S) }; + } + } + return lmsTable[keys[0]]; +} + +function normalCDF(z) { + var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911; + var sign = z < 0 ? -1 : 1; + z = Math.abs(z) / Math.sqrt(2); + var t = 1 / (1 + p * z); + var y = 1 - (((((a5*t+a4)*t)+a3)*t+a2)*t+a1)*t*Math.exp(-z*z); + return 0.5 * (1 + sign * y); +} + +export function calcBMIPercentile(bmi, L, M, S) { + var z; + if (Math.abs(L) < 0.001) z = Math.log(bmi / M) / S; + else z = (Math.pow(bmi / M, L) - 1) / (L * S); + var p = normalCDF(z); + return { z: z, percentile: Math.round(p * 10000) / 100 }; +} + +export function valueFromLMS(L, M, S, z) { + if (Math.abs(L) < 0.001) return M * Math.exp(S * z); + return M * Math.pow(1 + L * S * z, 1 / L); +} + +export function classifyBMI(percentile, bmi, lms) { + var bmi95 = valueFromLMS(lms.L, lms.M, lms.S, 1.645); + var pctOf95 = (bmi / bmi95) * 100; + + if (percentile >= 95) { + if (pctOf95 >= 140) return { text: 'Class 3 Severe Obesity', color: '#7f1d1d', bg: '#fecaca', pctOf95: pctOf95, bmi95: bmi95 }; + if (pctOf95 >= 120) return { text: 'Class 2 Severe Obesity', color: '#dc2626', bg: '#fee2e2', pctOf95: pctOf95, bmi95: bmi95 }; + return { text: 'Obese (Class 1)', color: '#ef4444', bg: '#fee2e2', pctOf95: pctOf95, bmi95: bmi95 }; + } + if (percentile >= 85) return { text: 'Overweight', color: '#f97316', bg: '#ffedd5', pctOf95: pctOf95, bmi95: bmi95 }; + if (percentile >= 5) return { text: 'Healthy Weight', color: '#10b981', bg: '#d1fae5', pctOf95: pctOf95, bmi95: bmi95 }; + return { text: 'Underweight', color: '#f59e0b', bg: '#fef3c7', pctOf95: pctOf95, bmi95: bmi95 }; +} + +export function calculateBMIAssessment(lmsData, sex, ageYears, weightKg, heightCm) { + var bmi = weightKg / Math.pow(heightCm / 100, 2); + var ageMonths = Math.round(ageYears * 12); + if (ageMonths < 24) ageMonths = 24; + if (ageMonths > 240) ageMonths = 240; + var lms = interpolateLMS(lmsData[sex], ageMonths); + var result = calcBMIPercentile(bmi, lms.L, lms.M, lms.S); + var classification = classifyBMI(result.percentile, bmi, lms); + return { + ageMonths: ageMonths, + bmi: bmi, + z: result.z, + percentile: result.percentile, + lms: lms, + classification: classification + }; +} diff --git a/test/calculators-data.test.js b/test/calculators-data.test.js index 5918a67..b2e2b66 100644 --- a/test/calculators-data.test.js +++ b/test/calculators-data.test.js @@ -224,3 +224,49 @@ test('AAP bilirubin sample calculation interpolates fractional age', async () => assert.equal(assessment.abovePhototherapy, false); assert.equal(assessment.status, 'below'); }); + +test('bmi-lms.json preserves CDC BMI-for-age anchors', () => { + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'bmi-lms.json'), 'utf8'); + const data = JSON.parse(raw); + assert.equal(data.version, '1.0'); + assert.deepEqual(data.lms.male['24'], { L: -1.982374, M: 16.5478, S: 0.080127 }); + assert.deepEqual(data.lms.male['120'], { L: -2.765648, M: 16.6461, S: 0.120112 }); + assert.deepEqual(data.lms.female['96'], { L: -2.617192, M: 15.827, S: 0.117159 }); + assert.deepEqual(data.lms.female['240'], { L: -2.342797, M: 21.7219, S: 0.153241 }); +}); + +test('BMI sample calculation preserves healthy-weight result', async () => { + const bmi = await import('../public/js/calculators/bmi.js'); + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'bmi-lms.json'), 'utf8'); + const data = JSON.parse(raw).lms; + const assessment = bmi.calculateBMIAssessment(data, 'male', 10, 32, 140); + assert.equal(assessment.ageMonths, 120); + assert.equal(Number(assessment.bmi.toFixed(1)), 16.3); + assert.equal(Number(assessment.z.toFixed(2)), -0.17); + assert.equal(assessment.percentile, 43.42); + assert.equal(assessment.classification.text, 'Healthy Weight'); +}); + +test('BMI sample calculation preserves obesity classification', async () => { + const bmi = await import('../public/js/calculators/bmi.js'); + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'bmi-lms.json'), 'utf8'); + const data = JSON.parse(raw).lms; + const assessment = bmi.calculateBMIAssessment(data, 'male', 12, 70, 150); + assert.equal(Number(assessment.bmi.toFixed(1)), 31.1); + assert.equal(assessment.percentile, 98.97); + assert.equal(assessment.classification.text, 'Class 2 Severe Obesity'); + assert.equal(Number(assessment.classification.pctOf95.toFixed(0)), 128); + assert.equal(Number(assessment.classification.bmi95.toFixed(1)), 24.2); +}); + +test('BMI sample calculation preserves fractional-age interpolation', async () => { + const bmi = await import('../public/js/calculators/bmi.js'); + const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'bmi-lms.json'), 'utf8'); + const data = JSON.parse(raw).lms; + const lms = bmi.interpolateLMS(data.male, 123); + assert.equal(Number(lms.M.toFixed(4)), 16.7806); + const assessment = bmi.calculateBMIAssessment(data, 'male', 10.5, 40, 145); + assert.equal(assessment.ageMonths, 126); + assert.equal(assessment.percentile, 79.26); + assert.equal(assessment.classification.text, 'Healthy Weight'); +});