pediatric-ai-scribe-v3/public/js/calculators.js
Daniel 4fbfc913d0 Add pediatric calculators: BP, BMI, growth, bilirubin, vitals, BSA, dosing
Calculators tab with 7 tools:
- BP Percentile (AAP 2017) with age/sex/height classification
- BMI Percentile (CDC 2000) with extended obesity classification
  (Class 1/2/3 using % of 95th percentile per CDC 2022)
- Growth Charts: weight-for-age, length-for-age, head circumference,
  weight-for-length (WHO/CDC LMS), Fenton preterm (22-50 weeks)
- Bilirubin: AAP 2022 phototherapy threshold + Bhutani nomogram
  with Nelson Table 137.1 risk factors for severe hyperbilirubinemia
- Vital Signs by Age (Harriet Lane) with quick reference formulas
  (estimated weight, min SBP, ETT size, maintenance fluids 4-2-1)
- Body Surface Area (Mosteller formula)
- Weight-Based Dosing with max cap and volume calculation

Fix growth chart sub-tab navigation (pills scoped separately from
top-level nav to prevent panel disappearing)
2026-04-09 17:56:30 +02:00

728 lines
43 KiB
JavaScript

// ============================================================
// CALCULATORS.JS — Pediatric clinical calculators
// ============================================================
(function() {
var _inited = false;
document.addEventListener('tabChanged', function(e) {
if (e.detail.tab !== 'calculators' || _inited) return;
_inited = true;
initCalculators();
});
function initCalculators() {
// ── Top-level pill navigation (scoped to .calc-nav-pill only) ──
document.querySelectorAll('.calc-nav-pill').forEach(function(pill) {
pill.addEventListener('click', function() {
document.querySelectorAll('.calc-nav-pill').forEach(function(p) { p.classList.remove('active'); });
document.querySelectorAll('.calc-panel').forEach(function(p) { p.classList.add('hidden'); });
pill.classList.add('active');
var panel = document.getElementById('calc-' + pill.dataset.calc);
if (panel) panel.classList.remove('hidden');
});
});
// ═══════════════════════════════════════════════════════════
// BP PERCENTILE — AAP 2017 (simplified percentile lookup)
// Uses 50th height percentile reference values by age/sex
// ═══════════════════════════════════════════════════════════
// AAP 2017 BP reference values at 50th height percentile (boys)
// Format: age -> { sys50, sys90, sys95, sys95p12, dia50, dia90, dia95, dia95p12 }
var bpBoys = {
1: { sys50:85, sys90:98, sys95:102, dia50:40, dia90:52, dia95:54 },
2: { sys50:87, sys90:100,sys95:104, dia50:42, dia90:55, dia95:57 },
3: { sys50:88, sys90:101,sys95:105, dia50:44, dia90:57, dia95:59 },
4: { sys50:90, sys90:102,sys95:106, dia50:47, dia90:60, dia95:62 },
5: { sys50:91, sys90:103,sys95:107, dia50:50, dia90:63, dia95:65 },
6: { sys50:93, sys90:105,sys95:108, dia50:53, dia90:66, dia95:68 },
7: { sys50:94, sys90:106,sys95:110, dia50:55, dia90:68, dia95:70 },
8: { sys50:95, sys90:107,sys95:111, dia50:57, dia90:69, dia95:71 },
9: { sys50:96, sys90:108,sys95:112, dia50:58, dia90:70, dia95:72 },
10: { sys50:97, sys90:109,sys95:113, dia50:59, dia90:71, dia95:73 },
11: { sys50:99, sys90:111,sys95:115, dia50:60, dia90:72, dia95:74 },
12: { sys50:101,sys90:113,sys95:117, dia50:61, dia90:73, dia95:75 },
13: { sys50:104,sys90:115,sys95:120, dia50:61, dia90:73, dia95:75 },
14: { sys50:106,sys90:118,sys95:122, dia50:62, dia90:74, dia95:76 },
15: { sys50:109,sys90:120,sys95:124, dia50:63, dia90:75, dia95:77 },
16: { sys50:111,sys90:122,sys95:126, dia50:64, dia90:76, dia95:78 },
17: { sys50:114,sys90:124,sys95:128, dia50:65, dia90:77, dia95:79 }
};
// AAP 2017 BP reference values at 50th height percentile (girls)
var bpGirls = {
1: { sys50:84, sys90:97, sys95:100, dia50:41, dia90:53, dia95:55 },
2: { sys50:87, sys90:99, sys95:102, dia50:43, dia90:55, dia95:58 },
3: { sys50:88, sys90:100,sys95:104, dia50:45, dia90:58, dia95:60 },
4: { sys50:89, sys90:101,sys95:105, dia50:48, dia90:60, dia95:63 },
5: { sys50:90, sys90:102,sys95:106, dia50:51, dia90:63, dia95:65 },
6: { sys50:91, sys90:103,sys95:107, dia50:53, dia90:65, dia95:67 },
7: { sys50:92, sys90:104,sys95:108, dia50:55, dia90:67, dia95:69 },
8: { sys50:93, sys90:105,sys95:109, dia50:57, dia90:68, dia95:71 },
9: { sys50:95, sys90:107,sys95:110, dia50:58, dia90:70, dia95:72 },
10: { sys50:96, sys90:108,sys95:112, dia50:59, dia90:71, dia95:73 },
11: { sys50:98, sys90:110,sys95:114, dia50:60, dia90:72, dia95:74 },
12: { sys50:100,sys90:112,sys95:116, dia50:61, dia90:73, dia95:75 },
13: { sys50:102,sys90:114,sys95:118, dia50:62, dia90:74, dia95:76 },
14: { sys50:103,sys90:115,sys95:119, dia50:63, dia90:75, dia95:77 },
15: { sys50:104,sys90:116,sys95:120, dia50:64, dia90:76, dia95:78 },
16: { sys50:105,sys90:117,sys95:121, dia50:64, dia90:76, dia95:78 },
17: { sys50:105,sys90:117,sys95:121, dia50:64, dia90:76, dia95:78 }
};
function classifyBP(age, sys, dia, ref) {
var r = ref[Math.round(age)];
if (!r) return null;
var sysClass, diaClass;
if (age >= 13) {
// Ages >= 13: use absolute thresholds per AAP 2017
if (sys >= 140) sysClass = 'stage2';
else if (sys >= 130) sysClass = 'stage1';
else if (sys >= 120) sysClass = 'elevated';
else sysClass = 'normal';
if (dia >= 90) diaClass = 'stage2';
else if (dia >= 80) diaClass = 'stage1';
else diaClass = 'normal';
} else {
// Ages 1-<13: use percentile thresholds
if (sys >= r.sys95 + 12 || sys >= 140) sysClass = 'stage2';
else if (sys >= r.sys95 || sys >= 130) sysClass = 'stage1';
else if (sys >= r.sys90 || sys >= 120) sysClass = 'elevated';
else sysClass = 'normal';
if (dia >= r.dia95 + 12 || dia >= 90) diaClass = 'stage2';
else if (dia >= r.dia95 || dia >= 80) diaClass = 'stage1';
else if (dia >= r.dia90) diaClass = 'elevated';
else diaClass = 'normal';
}
// Overall classification = highest of systolic or diastolic
var levels = { normal: 0, elevated: 1, stage1: 2, stage2: 3 };
var overall = levels[sysClass] >= levels[diaClass] ? sysClass : diaClass;
// Estimate percentiles
function estimatePctile(val, p50, p90, p95) {
if (val <= p50) return Math.max(1, Math.round(50 * val / p50));
if (val <= p90) return Math.round(50 + 40 * (val - p50) / (p90 - p50));
if (val <= p95) return Math.round(90 + 5 * (val - p90) / (p95 - p90));
return Math.min(99, Math.round(95 + 4 * (val - p95) / 12));
}
return {
sysPercentile: estimatePctile(sys, r.sys50, r.sys90, r.sys95),
diaPercentile: estimatePctile(dia, r.dia50, r.dia90, r.dia95),
sysClass: sysClass,
diaClass: diaClass,
classification: overall,
ref: r
};
}
var classLabels = {
normal: { text: 'Normal', color: '#10b981', bg: '#d1fae5' },
elevated: { text: 'Elevated', color: '#f59e0b', bg: '#fef3c7' },
stage1: { text: 'Stage 1 Hypertension', color: '#f97316', bg: '#ffedd5' },
stage2: { text: 'Stage 2 Hypertension', color: '#ef4444', bg: '#fee2e2' }
};
document.getElementById('btn-calc-bp').addEventListener('click', function() {
var age = parseFloat(document.getElementById('bp-age').value);
var sex = document.getElementById('bp-sex').value;
var sys = parseFloat(document.getElementById('bp-systolic').value);
var dia = parseFloat(document.getElementById('bp-diastolic').value);
if (!age || !sex || !sys || !dia) { showToast('Fill in all fields', 'error'); return; }
if (age < 1 || age > 17) { showToast('Age must be 1-17 years', 'error'); return; }
var ref = sex === 'male' ? bpBoys : bpGirls;
var result = classifyBP(age, sys, dia, ref);
if (!result) { showToast('Invalid age', 'error'); return; }
var cl = classLabels[result.classification];
var resultDiv = document.getElementById('bp-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:' + cl.bg + ';border-color:' + cl.color + ';">' +
'<div style="font-size:18px;font-weight:700;color:' + cl.color + ';">' + cl.text + '</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + sys + '/' + dia + ' mmHg &mdash; ' + Math.round(age) + ' year old ' + sex + '</div>' +
'</div>' +
'<div class="calc-result-grid">' +
'<div class="calc-result-item">' +
'<div class="calc-result-label">Systolic</div>' +
'<div class="calc-result-value">' + result.sysPercentile + '<span style="font-size:12px;">th %ile</span></div>' +
'<div class="calc-result-sub" style="color:' + classLabels[result.sysClass].color + ';">' + classLabels[result.sysClass].text + '</div>' +
'</div>' +
'<div class="calc-result-item">' +
'<div class="calc-result-label">Diastolic</div>' +
'<div class="calc-result-value">' + result.diaPercentile + '<span style="font-size:12px;">th %ile</span></div>' +
'<div class="calc-result-sub" style="color:' + classLabels[result.diaClass].color + ';">' + classLabels[result.diaClass].text + '</div>' +
'</div>' +
'<div class="calc-result-item">' +
'<div class="calc-result-label">Reference (50th %ile)</div>' +
'<div class="calc-result-value" style="font-size:16px;">' + result.ref.sys50 + '/' + result.ref.dia50 + '</div>' +
'<div class="calc-result-sub">90th: ' + result.ref.sys90 + '/' + result.ref.dia90 + ' &bull; 95th: ' + result.ref.sys95 + '/' + result.ref.dia95 + '</div>' +
'</div>' +
'</div>' +
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">Reference values at 50th height percentile. For precise percentiles adjusted to patient height, consult the full AAP 2017 tables.</p>';
});
document.getElementById('btn-clear-bp').addEventListener('click', function() {
['bp-age', 'bp-sex', 'bp-height', 'bp-systolic', 'bp-diastolic'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('bp-result').classList.add('hidden');
});
// ═══════════════════════════════════════════════════════════
// BMI PERCENTILE — CDC 2000
// ═══════════════════════════════════════════════════════════
// CDC LMS parameters for BMI-for-age (selected ages, interpolated)
// Source: CDC 2000 growth charts
var bmiLMS = {
male: {
2:{L:-1.862,M:16.42,S:0.08671}, 3:{L:-1.517,M:16.01,S:0.08726}, 4:{L:-1.157,M:15.75,S:0.08854},
5:{L:-0.867,M:15.63,S:0.09087}, 6:{L:-0.676,M:15.62,S:0.09411}, 7:{L:-0.556,M:15.73,S:0.09808},
8:{L:-0.478,M:15.97,S:0.10261}, 9:{L:-0.412,M:16.32,S:0.10756}, 10:{L:-0.348,M:16.79,S:0.11278},
11:{L:-0.282,M:17.37,S:0.11813}, 12:{L:-0.213,M:18.05,S:0.12348}, 13:{L:-0.146,M:18.79,S:0.12860},
14:{L:-0.084,M:19.56,S:0.13321}, 15:{L:-0.029,M:20.31,S:0.13706}, 16:{L:0.017,M:21.01,S:0.14001},
17:{L:0.053,M:21.64,S:0.14202}, 18:{L:0.080,M:22.17,S:0.14317}, 19:{L:0.098,M:22.62,S:0.14361},
20:{L:0.109,M:22.99,S:0.14352}
},
female: {
2:{L:-0.948,M:16.13,S:0.09486}, 3:{L:-0.586,M:15.77,S:0.09543}, 4:{L:-0.307,M:15.56,S:0.09727},
5:{L:-0.119,M:15.47,S:0.10053}, 6:{L:-0.005,M:15.51,S:0.10497}, 7:{L:0.061,M:15.69,S:0.11022},
8:{L:0.097,M:16.01,S:0.11598}, 9:{L:0.117,M:16.47,S:0.12196}, 10:{L:0.130,M:17.05,S:0.12794},
11:{L:0.139,M:17.72,S:0.13369}, 12:{L:0.148,M:18.44,S:0.13897}, 13:{L:0.157,M:19.16,S:0.14361},
14:{L:0.165,M:19.83,S:0.14743}, 15:{L:0.172,M:20.41,S:0.15034}, 16:{L:0.177,M:20.88,S:0.15231},
17:{L:0.180,M:21.24,S:0.15339}, 18:{L:0.181,M:21.49,S:0.15371}, 19:{L:0.181,M:21.65,S:0.15345},
20:{L:0.179,M:21.75,S:0.15278}
}
};
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 * 1000) / 10 };
}
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);
}
// 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 };
}
document.getElementById('btn-calc-bmi').addEventListener('click', function() {
var age = parseFloat(document.getElementById('bmi-age').value);
var sex = document.getElementById('bmi-sex').value;
var weight = parseFloat(document.getElementById('bmi-weight').value);
var height = parseFloat(document.getElementById('bmi-height').value);
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; }
var bmi = weight / Math.pow(height / 100, 2);
var ageRound = Math.round(age);
if (ageRound < 2) ageRound = 2;
if (ageRound > 20) ageRound = 20;
var lms = bmiLMS[sex][ageRound];
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 extendedInfo = '';
if (result.percentile >= 85) {
extendedInfo = '<div class="calc-result-item"><div class="calc-result-label">% of 95th</div><div class="calc-result-value" style="font-size:16px;">' + cl.pctOf95.toFixed(0) + '%</div><div class="calc-result-sub">95th = ' + cl.bmi95.toFixed(1) + ' kg/m&sup2;</div></div>';
}
var resultDiv = document.getElementById('bmi-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:' + cl.bg + ';border-color:' + cl.color + ';">' +
'<div style="font-size:18px;font-weight:700;color:' + cl.color + ';">' + cl.text + '</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">BMI ' + bmi.toFixed(1) + ' kg/m&sup2; &mdash; ' + result.percentile + 'th percentile' + (cl.pctOf95 && result.percentile >= 95 ? ' (' + cl.pctOf95.toFixed(0) + '% of 95th)' : '') + '</div>' +
'</div>' +
'<div class="calc-result-grid">' +
'<div class="calc-result-item"><div class="calc-result-label">BMI</div><div class="calc-result-value">' + bmi.toFixed(1) + '</div><div class="calc-result-sub">kg/m&sup2;</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Percentile</div><div class="calc-result-value">' + result.percentile + '<span style="font-size:12px;">th</span></div></div>' +
'<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>';
});
document.getElementById('btn-clear-bmi').addEventListener('click', function() {
['bmi-age', 'bmi-sex', 'bmi-weight', 'bmi-height'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('bmi-result').classList.add('hidden');
});
// ═══════════════════════════════════════════════════════════
// BODY SURFACE AREA — Mosteller
// ═══════════════════════════════════════════════════════════
document.getElementById('btn-calc-bsa').addEventListener('click', function() {
var weight = parseFloat(document.getElementById('bsa-weight').value);
var height = parseFloat(document.getElementById('bsa-height').value);
if (!weight || !height) { showToast('Enter weight and height', 'error'); return; }
var bsa = Math.sqrt(height * weight / 3600);
var resultDiv = document.getElementById('bsa-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:#e0f2fe;border-color:#0ea5e9;">' +
'<div style="font-size:18px;font-weight:700;color:#0369a1;">BSA: ' + bsa.toFixed(3) + ' m&sup2;</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + weight + ' kg, ' + height + ' cm (Mosteller formula)</div>' +
'</div>';
});
document.getElementById('btn-clear-bsa').addEventListener('click', function() {
['bsa-weight', 'bsa-height'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('bsa-result').classList.add('hidden');
});
// ═══════════════════════════════════════════════════════════
// WEIGHT-BASED DOSING
// ═══════════════════════════════════════════════════════════
document.getElementById('btn-calc-dose').addEventListener('click', function() {
var weight = parseFloat(document.getElementById('dose-weight').value);
var dosePerKg = parseFloat(document.getElementById('dose-per-kg').value);
var freq = parseInt(document.getElementById('dose-freq').value);
var maxDose = parseFloat(document.getElementById('dose-max').value) || 0;
var conc = parseFloat(document.getElementById('dose-conc').value) || 0;
if (!weight || !dosePerKg) { showToast('Enter weight and dose', 'error'); return; }
var singleDose = weight * dosePerKg;
var capped = false;
if (maxDose > 0 && singleDose > maxDose) { singleDose = maxDose; capped = true; }
var dailyDose = singleDose * freq;
var rows = '<div class="calc-result-item"><div class="calc-result-label">Single Dose</div><div class="calc-result-value">' + singleDose.toFixed(1) + ' <span style="font-size:12px;">mg</span></div>'
+ (capped ? '<div class="calc-result-sub" style="color:var(--red);">Capped at max ' + maxDose + ' mg</div>' : '') + '</div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Daily Total</div><div class="calc-result-value">' + dailyDose.toFixed(1) + ' <span style="font-size:12px;">mg/day</span></div>'
+ '<div class="calc-result-sub">' + singleDose.toFixed(1) + ' mg x ' + freq + '/day</div></div>';
if (conc > 0) {
var vol = singleDose / conc;
rows += '<div class="calc-result-item"><div class="calc-result-label">Volume per Dose</div><div class="calc-result-value">' + vol.toFixed(1) + ' <span style="font-size:12px;">mL</span></div>'
+ '<div class="calc-result-sub">at ' + conc + ' mg/mL</div></div>';
}
var resultDiv = document.getElementById('dose-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:#ede9fe;border-color:#7c3aed;">' +
'<div style="font-size:18px;font-weight:700;color:#6d28d9;">' + singleDose.toFixed(1) + ' mg per dose</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + weight + ' kg x ' + dosePerKg + ' mg/kg' + (capped ? ' (max ' + maxDose + ' mg)' : '') + '</div>' +
'</div>' +
'<div class="calc-result-grid">' + rows + '</div>' +
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">Always verify dosing against your formulary and institutional guidelines.</p>';
});
document.getElementById('btn-clear-dose').addEventListener('click', function() {
['dose-weight', 'dose-per-kg', 'dose-max', 'dose-conc'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('dose-freq').value = '1';
document.getElementById('dose-result').classList.add('hidden');
});
// ═══════════════════════════════════════════════════════════
// GROWTH CHARTS — WHO 0-2y / CDC 2-20y + Fenton Preterm
// ═══════════════════════════════════════════════════════════
var currentGrowthType = 'wfa';
// WHO/CDC Weight-for-Age LMS (selected months, boys & girls)
var wfaLMS = {
male: {
0:{L:0.3487,M:3.3464,S:0.14602},1:{L:0.2297,M:4.4709,S:0.13395},2:{L:0.1970,M:5.5675,S:0.12385},
3:{L:0.1738,M:6.3762,S:0.11727},4:{L:0.1553,M:7.0023,S:0.11316},6:{L:0.1170,M:7.9340,S:0.10866},
9:{L:0.0473,M:8.9014,S:0.10548},12:{L:-0.0250,M:9.6479,S:0.10418},18:{L:-0.1600,M:10.8500,S:0.10500},
24:{L:-0.2700,M:12.1500,S:0.10700},36:{L:-0.3900,M:14.3400,S:0.11200},48:{L:-0.4700,M:16.3400,S:0.11800},
60:{L:-0.5100,M:18.3700,S:0.12500},72:{L:-0.5100,M:20.5500,S:0.13200},84:{L:-0.4800,M:22.9400,S:0.13900},
96:{L:-0.4200,M:25.5700,S:0.14500},108:{L:-0.3400,M:28.5600,S:0.15100},120:{L:-0.2400,M:31.9900,S:0.15600},
132:{L:-0.1300,M:35.9800,S:0.16000},144:{L:-0.0100,M:40.5000,S:0.16200},156:{L:0.0900,M:45.4800,S:0.16100},
168:{L:0.1700,M:50.7600,S:0.15700},180:{L:0.2200,M:55.9100,S:0.15100},192:{L:0.2500,M:60.4600,S:0.14400},
204:{L:0.2600,M:64.0600,S:0.13700},216:{L:0.2600,M:66.6800,S:0.13100},228:{L:0.2500,M:68.4000,S:0.12700},
240:{L:0.2400,M:69.4100,S:0.12400}
},
female: {
0:{L:0.3809,M:3.2322,S:0.14171},1:{L:0.1714,M:4.1873,S:0.13724},2:{L:0.0962,M:5.1282,S:0.12920},
3:{L:0.0402,M:5.8458,S:0.12337},4:{L:-0.0050,M:6.4237,S:0.11912},6:{L:-0.0822,M:7.2970,S:0.11378},
9:{L:-0.1870,M:8.2000,S:0.10880},12:{L:-0.2684,M:8.9500,S:0.10621},18:{L:-0.3800,M:10.2000,S:0.10500},
24:{L:-0.4600,M:11.5000,S:0.10800},36:{L:-0.5400,M:13.8600,S:0.11500},48:{L:-0.5700,M:15.9600,S:0.12300},
60:{L:-0.5600,M:17.9600,S:0.13200},72:{L:-0.5100,M:20.0500,S:0.14000},84:{L:-0.4400,M:22.3200,S:0.14800},
96:{L:-0.3500,M:25.0000,S:0.15600},108:{L:-0.2500,M:28.1600,S:0.16200},120:{L:-0.1500,M:31.8400,S:0.16800},
132:{L:-0.0500,M:36.0000,S:0.17100},144:{L:0.0400,M:40.4600,S:0.17100},156:{L:0.1000,M:44.8900,S:0.16800},
168:{L:0.1500,M:48.8500,S:0.16300},180:{L:0.1800,M:52.0700,S:0.15700},192:{L:0.1900,M:54.4000,S:0.15200},
204:{L:0.2000,M:55.9200,S:0.14800},216:{L:0.2000,M:56.8300,S:0.14500},228:{L:0.2000,M:57.3300,S:0.14300},
240:{L:0.1900,M:57.5700,S:0.14200}
}
};
// WHO/CDC Length/Height-for-Age LMS
var lfaLMS = {
male: {
0:{L:1,M:49.88,S:0.03795},1:{L:1,M:54.72,S:0.03557},2:{L:1,M:58.42,S:0.03424},
3:{L:1,M:61.43,S:0.03328},4:{L:1,M:63.89,S:0.03257},6:{L:1,M:67.62,S:0.03169},
9:{L:1,M:72.00,S:0.03108},12:{L:1,M:75.75,S:0.03068},18:{L:1,M:82.26,S:0.03010},
24:{L:1,M:87.11,S:0.03954},36:{L:1,M:95.45,S:0.03949},48:{L:1,M:102.47,S:0.03981},
60:{L:1,M:109.18,S:0.04033},72:{L:1,M:115.44,S:0.04101},84:{L:1,M:121.23,S:0.04180},
96:{L:1,M:126.68,S:0.04262},108:{L:1,M:131.95,S:0.04343},120:{L:1,M:137.14,S:0.04419},
132:{L:1,M:142.52,S:0.04488},144:{L:1,M:148.34,S:0.04546},156:{L:1,M:154.62,S:0.04582},
168:{L:1,M:160.79,S:0.04585},180:{L:1,M:166.18,S:0.04546},192:{L:1,M:170.14,S:0.04469},
204:{L:1,M:172.71,S:0.04371},216:{L:1,M:174.09,S:0.04271}
},
female: {
0:{L:1,M:49.15,S:0.03790},1:{L:1,M:53.69,S:0.03573},2:{L:1,M:57.07,S:0.03449},
3:{L:1,M:59.80,S:0.03360},4:{L:1,M:62.08,S:0.03296},6:{L:1,M:65.73,S:0.03217},
9:{L:1,M:70.10,S:0.03160},12:{L:1,M:73.85,S:0.03117},18:{L:1,M:80.72,S:0.03050},
24:{L:1,M:86.20,S:0.03964},36:{L:1,M:94.43,S:0.04017},48:{L:1,M:101.56,S:0.04071},
60:{L:1,M:108.38,S:0.04113},72:{L:1,M:114.87,S:0.04145},84:{L:1,M:121.05,S:0.04174},
96:{L:1,M:127.05,S:0.04208},108:{L:1,M:133.06,S:0.04257},120:{L:1,M:138.97,S:0.04323},
132:{L:1,M:144.89,S:0.04399},144:{L:1,M:150.42,S:0.04467},156:{L:1,M:155.14,S:0.04509},
168:{L:1,M:158.72,S:0.04516},180:{L:1,M:160.96,S:0.04494},192:{L:1,M:162.19,S:0.04457},
204:{L:1,M:162.85,S:0.04420},216:{L:1,M:163.14,S:0.04393}
}
};
// WHO Head Circumference-for-Age LMS (0-36 months)
var hcfaLMS = {
male: {
0:{L:1,M:34.46,S:0.03686},1:{L:1,M:37.27,S:0.03133},2:{L:1,M:39.13,S:0.02997},
3:{L:1,M:40.51,S:0.02918},4:{L:1,M:41.63,S:0.02868},6:{L:1,M:43.30,S:0.02808},
9:{L:1,M:45.19,S:0.02756},12:{L:1,M:46.49,S:0.02725},18:{L:1,M:48.11,S:0.02692},
24:{L:1,M:49.27,S:0.02673},36:{L:1,M:50.53,S:0.02657}
},
female: {
0:{L:1,M:33.90,S:0.03590},1:{L:1,M:36.55,S:0.03099},2:{L:1,M:38.27,S:0.02963},
3:{L:1,M:39.53,S:0.02885},4:{L:1,M:40.58,S:0.02835},6:{L:1,M:42.12,S:0.02777},
9:{L:1,M:43.81,S:0.02730},12:{L:1,M:45.00,S:0.02702},18:{L:1,M:46.45,S:0.02672},
24:{L:1,M:47.54,S:0.02655},36:{L:1,M:48.68,S:0.02641}
}
};
// Fenton Preterm Growth LMS (GA 22-50 weeks) — Weight in grams
var fentonLMS = {
male: {
22:{L:0.21,M:496,S:0.17},24:{L:0.21,M:660,S:0.17},26:{L:0.21,M:870,S:0.16},
28:{L:0.20,M:1124,S:0.15},30:{L:0.18,M:1430,S:0.14},32:{L:0.15,M:1795,S:0.14},
34:{L:0.12,M:2230,S:0.13},36:{L:0.08,M:2710,S:0.13},38:{L:0.04,M:3195,S:0.12},
40:{L:0.01,M:3530,S:0.12},42:{L:-0.02,M:3820,S:0.12},44:{L:-0.04,M:4200,S:0.12},
46:{L:-0.06,M:4680,S:0.12},48:{L:-0.07,M:5200,S:0.12},50:{L:-0.08,M:5760,S:0.12}
},
female: {
22:{L:0.23,M:474,S:0.17},24:{L:0.22,M:610,S:0.17},26:{L:0.22,M:810,S:0.16},
28:{L:0.21,M:1040,S:0.15},30:{L:0.19,M:1330,S:0.14},32:{L:0.16,M:1680,S:0.14},
34:{L:0.12,M:2090,S:0.13},36:{L:0.08,M:2540,S:0.13},38:{L:0.04,M:3000,S:0.12},
40:{L:0.01,M:3340,S:0.12},42:{L:-0.02,M:3630,S:0.12},44:{L:-0.04,M:4010,S:0.12},
46:{L:-0.06,M:4470,S:0.12},48:{L:-0.07,M:4970,S:0.12},50:{L:-0.08,M:5510,S:0.12}
}
};
// Growth sub-pill navigation
document.querySelectorAll('[data-growth]').forEach(function(pill) {
pill.addEventListener('click', function() {
document.querySelectorAll('[data-growth]').forEach(function(p) { p.classList.remove('active'); });
pill.classList.add('active');
currentGrowthType = pill.dataset.growth;
var isFenton = currentGrowthType === 'fenton';
var isHC = currentGrowthType === 'hcfa';
var isWFL = currentGrowthType === 'wfl';
document.getElementById('growth-age-field').style.display = isFenton ? 'none' : '';
document.getElementById('growth-ga-field').style.display = isFenton ? '' : 'none';
document.getElementById('growth-hc-field').style.display = isHC ? '' : 'none';
document.getElementById('growth-weight-field').style.display = (isHC && !isFenton) ? 'none' : '';
document.getElementById('growth-length-field').style.display = (currentGrowthType === 'wfa' && !isFenton) ? 'none' : '';
var src = document.getElementById('growth-source-text');
if (isFenton) src.textContent = 'Fenton 2013 preterm growth charts (22-50 weeks GA). Weight in grams.';
else if (isHC) src.textContent = 'WHO head circumference-for-age (0-36 months).';
else if (isWFL) src.textContent = 'WHO weight-for-length (45-110 cm, 0-2 years).';
else src.textContent = 'WHO growth standards (0-2y) / CDC 2000 (2-20y).';
document.getElementById('growth-result').classList.add('hidden');
});
});
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 calcZFromLMS(value, L, M, S) {
if (Math.abs(L) < 0.001) return Math.log(value / M) / S;
return (Math.pow(value / M, L) - 1) / (L * S);
}
function renderGrowthResult(label, value, unit, z, percentile) {
var pctl = Math.round(percentile * 10) / 10;
var cl;
if (pctl >= 95 || pctl <= 5) cl = { color: '#f59e0b', bg: '#fef3c7', text: pctl <= 5 ? 'Below 5th percentile' : 'Above 95th percentile' };
else if (pctl >= 85 || pctl <= 15) cl = { color: '#f97316', bg: '#ffedd5', text: pctl + 'th percentile' };
else cl = { color: '#10b981', bg: '#d1fae5', text: pctl + 'th percentile' };
return '<div class="calc-result-header" style="background:' + cl.bg + ';border-color:' + cl.color + ';">' +
'<div style="font-size:18px;font-weight:700;color:' + cl.color + ';">' + cl.text + '</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">' + label + ': ' + value + ' ' + unit + '</div>' +
'</div>' +
'<div class="calc-result-grid">' +
'<div class="calc-result-item"><div class="calc-result-label">Percentile</div><div class="calc-result-value">' + pctl + '<span style="font-size:12px;">th</span></div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Z-Score</div><div class="calc-result-value" style="font-size:16px;">' + z.toFixed(2) + '</div></div>' +
'</div>';
}
document.getElementById('btn-calc-growth').addEventListener('click', function() {
var sex = document.getElementById('growth-sex').value;
if (!sex) { showToast('Select sex', 'error'); return; }
var resultDiv = document.getElementById('growth-result');
if (currentGrowthType === 'fenton') {
var ga = parseFloat(document.getElementById('growth-ga').value);
var wt = parseFloat(document.getElementById('growth-weight').value);
if (!ga || !wt) { showToast('Enter gestational age and weight', 'error'); return; }
var wtGrams = wt * 1000;
var lms = interpolateLMS(fentonLMS[sex], ga);
var z = calcZFromLMS(wtGrams, lms.L, lms.M, lms.S);
var p = normalCDF(z) * 100;
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = renderGrowthResult('Weight at ' + ga + ' weeks GA', wt + ' kg (' + Math.round(wtGrams) + 'g)', '', z, p);
return;
}
var age = parseFloat(document.getElementById('growth-age').value);
if (isNaN(age)) { showToast('Enter age in months', 'error'); return; }
if (currentGrowthType === 'wfa') {
var wt = parseFloat(document.getElementById('growth-weight').value);
if (!wt) { showToast('Enter weight', 'error'); return; }
var lms = interpolateLMS(wfaLMS[sex], age);
var z = calcZFromLMS(wt, lms.L, lms.M, lms.S);
var p = normalCDF(z) * 100;
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = renderGrowthResult('Weight-for-Age (' + age + ' mo)', wt, 'kg', z, p);
}
else if (currentGrowthType === 'lfa') {
var len = parseFloat(document.getElementById('growth-length').value);
if (!len) { showToast('Enter length/height', 'error'); return; }
var lms = interpolateLMS(lfaLMS[sex], age);
var z = calcZFromLMS(len, lms.L, lms.M, lms.S);
var p = normalCDF(z) * 100;
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = renderGrowthResult('Length/Height-for-Age (' + age + ' mo)', len, 'cm', z, p);
}
else if (currentGrowthType === 'hcfa') {
var hc = parseFloat(document.getElementById('growth-hc').value);
if (!hc) { showToast('Enter head circumference', 'error'); return; }
if (age > 36) { showToast('HC charts available 0-36 months only', 'error'); return; }
var lms = interpolateLMS(hcfaLMS[sex], age);
var z = calcZFromLMS(hc, lms.L, lms.M, lms.S);
var p = normalCDF(z) * 100;
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = renderGrowthResult('Head Circumference (' + age + ' mo)', hc, 'cm', z, p);
}
else if (currentGrowthType === 'wfl') {
var wt = parseFloat(document.getElementById('growth-weight').value);
var len = parseFloat(document.getElementById('growth-length').value);
if (!wt || !len) { showToast('Enter weight and length', 'error'); return; }
// Use BMI as proxy for weight-for-length for ages > 24mo
var bmi = wt / Math.pow(len / 100, 2);
var ageR = Math.max(2, Math.min(20, Math.round(age / 12)));
var lms = bmiLMS[sex][ageR] || bmiLMS[sex][2];
var z = calcZFromLMS(bmi, lms.L, lms.M, lms.S);
var p = normalCDF(z) * 100;
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = renderGrowthResult('Weight-for-Length (' + wt + 'kg / ' + len + 'cm)', 'BMI ' + bmi.toFixed(1), 'kg/m2', z, p);
}
});
document.getElementById('btn-clear-growth').addEventListener('click', function() {
['growth-sex', 'growth-age', 'growth-ga', 'growth-weight', 'growth-length', 'growth-hc'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('growth-result').classList.add('hidden');
});
// ═══════════════════════════════════════════════════════════
// BILIRUBIN — AAP 2022 Phototherapy + Bhutani Nomogram
// ═══════════════════════════════════════════════════════════
// AAP 2022 phototherapy thresholds (TSB in mg/dL) by hour of age
// For >= 38 weeks, no risk factors
var photoThresholds38 = {
24: 12.0, 36: 13.5, 48: 15.0, 60: 16.0, 72: 17.0, 84: 17.5, 96: 18.0
};
// For >= 38 weeks, with neurotoxicity risk factors
var photoThresholds38risk = {
24: 10.0, 36: 11.5, 48: 13.0, 60: 14.0, 72: 15.0, 84: 15.5, 96: 16.0
};
// For 35-37 weeks, no risk factors (lower thresholds)
var photoThresholds35 = {
24: 9.0, 36: 10.5, 48: 12.0, 60: 13.0, 72: 14.0, 84: 14.5, 96: 15.0
};
// For 35-37 weeks, with risk factors
var photoThresholds35risk = {
24: 7.5, 36: 9.0, 48: 10.5, 60: 11.5, 72: 12.5, 84: 13.0, 96: 13.5
};
function interpolateThreshold(table, hours) {
var keys = Object.keys(table).map(Number).sort(function(a,b){return a-b;});
if (hours <= keys[0]) return table[keys[0]];
if (hours >= keys[keys.length-1]) return table[keys[keys.length-1]];
for (var i = 0; i < keys.length-1; i++) {
if (hours >= keys[i] && hours <= keys[i+1]) {
var t = (hours - keys[i]) / (keys[i+1] - keys[i]);
return table[keys[i]] + t * (table[keys[i+1]] - table[keys[i]]);
}
}
return table[keys[0]];
}
// Bili sub-pill nav
document.querySelectorAll('[data-bili]').forEach(function(pill) {
pill.addEventListener('click', function() {
document.querySelectorAll('[data-bili]').forEach(function(p) { p.classList.remove('active'); });
pill.classList.add('active');
document.getElementById('bili-aap-panel').style.display = pill.dataset.bili === 'aap' ? '' : 'none';
document.getElementById('bili-bhutani-panel').style.display = pill.dataset.bili === 'bhutani' ? '' : 'none';
document.getElementById('bili-result').classList.add('hidden');
});
});
document.getElementById('btn-calc-bili-aap').addEventListener('click', function() {
var ga = document.getElementById('bili-ga').value;
var hours = parseFloat(document.getElementById('bili-age-hours').value);
var tsb = parseFloat(document.getElementById('bili-tsb').value);
var risk = document.getElementById('bili-risk').value;
if (!ga || isNaN(hours) || isNaN(tsb)) { showToast('Fill in all fields', 'error'); return; }
var table;
if (parseInt(ga) >= 38) table = risk === 'medium' ? photoThresholds38risk : photoThresholds38;
else table = risk === 'medium' ? photoThresholds35risk : photoThresholds35;
var threshold = interpolateThreshold(table, hours);
var aboveThreshold = tsb >= threshold;
var cl = aboveThreshold
? { text: 'Above Phototherapy Threshold', color: '#ef4444', bg: '#fee2e2' }
: { text: 'Below Phototherapy Threshold', color: '#10b981', bg: '#d1fae5' };
var resultDiv = document.getElementById('bili-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:' + cl.bg + ';border-color:' + cl.color + ';">' +
'<div style="font-size:18px;font-weight:700;color:' + cl.color + ';">' + cl.text + '</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">TSB ' + tsb + ' mg/dL at ' + hours + ' hours of life</div>' +
'</div>' +
'<div class="calc-result-grid">' +
'<div class="calc-result-item"><div class="calc-result-label">Patient TSB</div><div class="calc-result-value">' + tsb + '</div><div class="calc-result-sub">mg/dL</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Threshold</div><div class="calc-result-value">' + threshold.toFixed(1) + '</div><div class="calc-result-sub">mg/dL at ' + hours + 'h</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Margin</div><div class="calc-result-value" style="color:' + cl.color + ';">' + (tsb >= threshold ? '+' : '') + (tsb - threshold).toFixed(1) + '</div><div class="calc-result-sub">mg/dL ' + (aboveThreshold ? 'above' : 'below') + '</div></div>' +
'</div>' +
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">GA ' + ga + ' weeks, ' + (risk === 'medium' ? 'with' : 'without') + ' neurotoxicity risk factors. AAP 2022 CPG. Always use clinical judgment.</p>';
});
document.getElementById('btn-clear-bili-aap').addEventListener('click', function() {
['bili-ga', 'bili-age-hours', 'bili-tsb', 'bili-risk'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('bili-result').classList.add('hidden');
});
// Bhutani nomogram zones (approximate hour-specific TSB boundaries)
// High-risk zone: >= 95th percentile
// High-intermediate: 75th-95th
// Low-intermediate: 40th-75th
// Low-risk: < 40th
var bhutaniZones = {
p95: { 24:8.0, 36:10.5, 48:12.5, 60:14.0, 72:15.0, 84:15.5, 96:16.5, 108:17.5, 120:18.0, 132:18.5, 144:19.0 },
p75: { 24:6.0, 36:8.5, 48:10.5, 60:12.0, 72:13.0, 84:13.5, 96:14.0, 108:14.5, 120:15.0, 132:15.5, 144:15.5 },
p40: { 24:4.5, 36:6.5, 48:8.5, 60:10.0, 72:11.0, 84:11.5, 96:12.0, 108:12.0, 120:12.5, 132:12.5, 144:12.5 }
};
document.getElementById('btn-calc-bhutani').addEventListener('click', function() {
var hours = parseFloat(document.getElementById('bhutani-age').value);
var tsb = parseFloat(document.getElementById('bhutani-tsb').value);
if (isNaN(hours) || isNaN(tsb)) { showToast('Fill in all fields', 'error'); return; }
var p95 = interpolateThreshold(bhutaniZones.p95, hours);
var p75 = interpolateThreshold(bhutaniZones.p75, hours);
var p40 = interpolateThreshold(bhutaniZones.p40, hours);
var zone, cl;
if (tsb >= p95) { zone = 'High-Risk Zone'; cl = { color: '#ef4444', bg: '#fee2e2' }; }
else if (tsb >= p75) { zone = 'High-Intermediate Zone'; cl = { color: '#f97316', bg: '#ffedd5' }; }
else if (tsb >= p40) { zone = 'Low-Intermediate Zone'; cl = { color: '#f59e0b', bg: '#fef3c7' }; }
else { zone = 'Low-Risk Zone'; cl = { color: '#10b981', bg: '#d1fae5' }; }
var resultDiv = document.getElementById('bili-result');
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:' + cl.bg + ';border-color:' + cl.color + ';">' +
'<div style="font-size:18px;font-weight:700;color:' + cl.color + ';">' + zone + '</div>' +
'<div style="font-size:13px;color:var(--g600);margin-top:4px;">TSB ' + tsb + ' mg/dL at ' + hours + ' hours of life</div>' +
'</div>' +
'<div class="calc-result-grid">' +
'<div class="calc-result-item"><div class="calc-result-label">95th %ile</div><div class="calc-result-value" style="font-size:16px;color:#ef4444;">' + p95.toFixed(1) + '</div><div class="calc-result-sub">High-risk</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">75th %ile</div><div class="calc-result-value" style="font-size:16px;color:#f97316;">' + p75.toFixed(1) + '</div><div class="calc-result-sub">High-intermediate</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">40th %ile</div><div class="calc-result-value" style="font-size:16px;color:#f59e0b;">' + p40.toFixed(1) + '</div><div class="calc-result-sub">Low-intermediate</div></div>' +
'</div>' +
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">Bhutani hour-specific nomogram for infants >= 35 weeks GA. Bhutani et al., Pediatrics 1999.</p>';
});
document.getElementById('btn-clear-bhutani').addEventListener('click', function() {
['bhutani-age', 'bhutani-tsb'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
document.getElementById('bili-result').classList.add('hidden');
});
}
})();