diff --git a/public/components/calculators.html b/public/components/calculators.html
new file mode 100644
index 0000000..6cf3030
--- /dev/null
+++ b/public/components/calculators.html
@@ -0,0 +1,424 @@
+
+
+
+
Learning Hub
@@ -262,6 +266,7 @@
+
@@ -348,6 +353,7 @@
+
diff --git a/public/js/calculators.js b/public/js/calculators.js
new file mode 100644
index 0000000..a3051ba
--- /dev/null
+++ b/public/js/calculators.js
@@ -0,0 +1,728 @@
+// ============================================================
+// 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 =
+ '' +
+ '' +
+ '
' +
+ '
Systolic
' +
+ '
' + result.sysPercentile + 'th %ile
' +
+ '
' + classLabels[result.sysClass].text + '
' +
+ '
' +
+ '
' +
+ '
Diastolic
' +
+ '
' + result.diaPercentile + 'th %ile
' +
+ '
' + classLabels[result.diaClass].text + '
' +
+ '
' +
+ '
' +
+ '
Reference (50th %ile)
' +
+ '
' + result.ref.sys50 + '/' + result.ref.dia50 + '
' +
+ '
90th: ' + result.ref.sys90 + '/' + result.ref.dia90 + ' • 95th: ' + result.ref.sys95 + '/' + result.ref.dia95 + '
' +
+ '
' +
+ '
' +
+ 'Reference values at 50th height percentile. For precise percentiles adjusted to patient height, consult the full AAP 2017 tables.
';
+ });
+
+ 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 = '% of 95th
' + cl.pctOf95.toFixed(0) + '%
95th = ' + cl.bmi95.toFixed(1) + ' kg/m²
';
+ }
+
+ var resultDiv = document.getElementById('bmi-result');
+ resultDiv.classList.remove('hidden');
+ resultDiv.innerHTML =
+ '' +
+ '' +
+ '
BMI
' + bmi.toFixed(1) + '
kg/m²
' +
+ '
Percentile
' + result.percentile + 'th
' +
+ '
Z-Score
' + result.z.toFixed(2) + '
' +
+ extendedInfo +
+ '
';
+ });
+
+ 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 =
+ '';
+ });
+
+ 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 = 'Single Dose
' + singleDose.toFixed(1) + ' mg
'
+ + (capped ? '
Capped at max ' + maxDose + ' mg
' : '') + '
'
+ + 'Daily Total
' + dailyDose.toFixed(1) + ' mg/day
'
+ + '
' + singleDose.toFixed(1) + ' mg x ' + freq + '/day
';
+
+ if (conc > 0) {
+ var vol = singleDose / conc;
+ rows += 'Volume per Dose
' + vol.toFixed(1) + ' mL
'
+ + '
at ' + conc + ' mg/mL
';
+ }
+
+ var resultDiv = document.getElementById('dose-result');
+ resultDiv.classList.remove('hidden');
+ resultDiv.innerHTML =
+ '' +
+ '' + rows + '
' +
+ 'Always verify dosing against your formulary and institutional guidelines.
';
+ });
+
+ 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 '' +
+ '' +
+ '
' +
+ '
Z-Score
' + z.toFixed(2) + '
' +
+ '
';
+ }
+
+ 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 =
+ '' +
+ '' +
+ '
Patient TSB
' + tsb + '
mg/dL
' +
+ '
Threshold
' + threshold.toFixed(1) + '
mg/dL at ' + hours + 'h
' +
+ '
Margin
' + (tsb >= threshold ? '+' : '') + (tsb - threshold).toFixed(1) + '
mg/dL ' + (aboveThreshold ? 'above' : 'below') + '
' +
+ '
' +
+ 'GA ' + ga + ' weeks, ' + (risk === 'medium' ? 'with' : 'without') + ' neurotoxicity risk factors. AAP 2022 CPG. Always use clinical judgment.
';
+ });
+
+ 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 =
+ '' +
+ '' +
+ '
95th %ile
' + p95.toFixed(1) + '
High-risk
' +
+ '
75th %ile
' + p75.toFixed(1) + '
High-intermediate
' +
+ '
40th %ile
' + p40.toFixed(1) + '
Low-intermediate
' +
+ '
' +
+ 'Bhutani hour-specific nomogram for infants >= 35 weeks GA. Bhutani et al., Pediatrics 1999.
';
+ });
+
+ 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');
+ });
+ }
+})();