extract calculator BMI data
This commit is contained in:
parent
903cd7eca8
commit
d79e578863
4 changed files with 523 additions and 46 deletions
380
public/data/calculators/bmi-lms.json
Normal file
380
public/data/calculators/bmi-lms.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
|||
'<div class="calc-result-item"><div class="calc-result-label">Z-Score</div><div class="calc-result-value" style="font-size:16px;">' + result.z.toFixed(2) + '</div></div>' +
|
||||
extendedInfo +
|
||||
'</div>';
|
||||
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');
|
||||
|
|
|
|||
82
public/js/calculators/bmi.js
Normal file
82
public/js/calculators/bmi.js
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue