import { parseAgeMonths, formatParsedAge } from './calculators/age.js'; import { calculateAapBilirubinAssessment, classifyBhutaniRisk, loadAapBilirubinThresholds, loadBhutaniZones } from './calculators/bilirubin.js'; import { calculateBMIAssessment, loadBmiLmsData } from './calculators/bmi.js'; import { calculateBPAssessment, computeBPPercentile, loadBpReferenceData } from './calculators/bp.js'; import { calculateBsa, calculateWeightBasedDose } from './calculators/dosing.js'; import { loadEquipmentData, renderEquipmentResult } from './calculators/equipment.js'; import { calculateGcs } from './calculators/gcs.js'; import { calcMidParentalHeight, calcZFromLMS, interpolateLMS, loadGrowthLmsData, normalCDF, renderGrowthResult, valueFromLMS } from './calculators/growth.js'; import { initImageLightbox } from './calculators/lightbox.js'; import { loadResusMedsData, renderResusMeds } from './calculators/resus.js'; import { loadVitalsData, renderVitalsResult } from './calculators/vitals.js'; // ============================================================ // CALCULATORS.JS — Pediatric clinical calculators // ============================================================ initImageLightbox(); var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'calculators' || _inited) return; _inited = true; initCalculators(); }); // Expose for debugging / other modules window._parseAgeMonths = parseAgeMonths; function initCalculators() { // ── Top-level pill navigation (scoped to .calc-nav-pill only) ── function activateCalcPill(key) { var pill = document.querySelector('.calc-nav-pill[data-calc="' + key + '"]'); if (!pill) return false; 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-' + key); if (panel) panel.classList.remove('hidden'); return true; } document.querySelectorAll('.calc-nav-pill').forEach(function(pill) { pill.addEventListener('click', function() { activateCalcPill(pill.dataset.calc); if (window.UIState) window.UIState.set('calc.pill', pill.dataset.calc); }); }); // Restore the last-selected pill across page reloads / sign-out-sign-in. if (window.UIState) { var saved = window.UIState.get('calc.pill'); if (saved) activateCalcPill(saved); } // ═══════════════════════════════════════════════════════════ // BP PERCENTILE — BCM/Rosner Quantile Spline Regression // AAP 2017 classification with exact percentile computation // ═══════════════════════════════════════════════════════════ // BP reference tables load from /data/calculators/bp-reference.json. 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' } }; var BP_REFERENCE = null; loadBpReferenceData() .then(function(data) { BP_REFERENCE = data; }) .catch(function(err) { console.warn('[calculators] BP reference data unavailable:', err && err.message); }); 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); var heightCm = parseFloat(document.getElementById('bp-height').value); if (!age || !sex || !sys || !dia || !heightCm) { showToast('Fill in all fields including height', 'error'); return; } if (age < 1 || age > 17) { showToast('Age must be 1-17 years', 'error'); return; } if (!BP_REFERENCE) { showToast('BP percentile data is still loading.', 'info'); return; } var assessment = calculateBPAssessment(BP_REFERENCE, age, sex, heightCm, sys, dia); var htResult = assessment.height; var sysResult = assessment.systolic; var diaResult = assessment.diastolic; var bpClass = assessment.classification; // Get 50th, 90th, 95th predicted values for display var sys50 = sysResult.predicted[49].toFixed(0), sys90 = sysResult.predicted[89].toFixed(0), sys95 = sysResult.predicted[94].toFixed(0); var dia50 = diaResult.predicted[49].toFixed(0), dia90 = diaResult.predicted[89].toFixed(0), dia95 = diaResult.predicted[94].toFixed(0); var cl = classLabels[bpClass.classification]; var resultDiv = document.getElementById('bp-result'); resultDiv.classList.remove('hidden'); resultDiv.innerHTML = '
BCM/Rosner quantile regression percentiles adjusted to patient height. AAP 2017 classification.
'; // Generate BP curves across ages 1-17 for this sex/height renderBPChart(age, sex, heightCm, sys, dia); }); function renderBPChart(patientAge, sex, heightCm, patientSys, patientDia) { if (typeof Chart === 'undefined') return; var container = document.getElementById('bp-chart-container'); if (!container) return; container.classList.remove('hidden'); if (_chartInstances['bp-chart-canvas']) { _chartInstances['bp-chart-canvas'].destroy(); _chartInstances['bp-chart-canvas'] = null; } // Generate percentile curves across ages 1-17 var sys50 = [], sys90 = [], sys95 = [], dia50 = [], dia90 = [], dia95 = []; for (var a = 1; a <= 17; a++) { var sr = computeBPPercentile(BP_REFERENCE, a, sex, heightCm, 'sys', 100); var dr = computeBPPercentile(BP_REFERENCE, a, sex, heightCm, 'dia', 60); sys50.push({x:a, y:Math.round(sr.predicted[49])}); sys90.push({x:a, y:Math.round(sr.predicted[89])}); sys95.push({x:a, y:Math.round(sr.predicted[94])}); dia50.push({x:a, y:Math.round(dr.predicted[49])}); dia90.push({x:a, y:Math.round(dr.predicted[89])}); dia95.push({x:a, y:Math.round(dr.predicted[94])}); } var datasets = [ { label: 'SBP 95th', data: sys95, borderColor: 'rgba(239,68,68,0.7)', borderWidth: 2, borderDash: [4,4], pointRadius: 0, fill: false, tension: 0.3 }, { label: 'SBP 90th', data: sys90, borderColor: 'rgba(249,115,22,0.6)', borderWidth: 1.5, borderDash: [3,3], pointRadius: 0, fill: false, tension: 0.3 }, { label: 'SBP 50th', data: sys50, borderColor: 'rgba(16,185,129,0.8)', borderWidth: 2, pointRadius: 0, fill: false, tension: 0.3 }, { label: 'DBP 95th', data: dia95, borderColor: 'rgba(239,68,68,0.4)', borderWidth: 1.5, borderDash: [4,4], pointRadius: 0, fill: false, tension: 0.3 }, { label: 'DBP 90th', data: dia90, borderColor: 'rgba(249,115,22,0.3)', borderWidth: 1, borderDash: [3,3], pointRadius: 0, fill: false, tension: 0.3 }, { label: 'DBP 50th', data: dia50, borderColor: 'rgba(16,185,129,0.4)', borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0.3 }, { label: 'Patient SBP', data: [{x:patientAge, y:patientSys}], backgroundColor: '#2563eb', borderColor: '#1d4ed8', pointRadius: 8, pointHoverRadius: 10, showLine: false, type: 'scatter', order: 0 }, { label: 'Patient DBP', data: [{x:patientAge, y:patientDia}], backgroundColor: '#7c3aed', borderColor: '#6d28d9', pointRadius: 8, pointHoverRadius: 10, pointStyle: 'triangle', showLine: false, type: 'scatter', order: 0 } ]; _chartInstances['bp-chart-canvas'] = new Chart(document.getElementById('bp-chart-canvas'), { type: 'line', data: { datasets: datasets }, options: { responsive: true, maintainAspectRatio: true, aspectRatio: 1.6, animation: { duration: 300 }, scales: { x: { type: 'linear', title: { display: true, text: 'Age (years)', font: { size: 12, weight: 'bold' } }, min: 1, max: 17, ticks: { stepSize: 1 }, grid: { color: 'rgba(0,0,0,0.05)' } }, y: { title: { display: true, text: 'Blood Pressure (mmHg)', font: { size: 12, weight: 'bold' } }, min: 30, grid: { color: 'rgba(0,0,0,0.05)' } } }, plugins: { legend: { display: true, position: 'bottom', labels: { font: { size: 10 }, usePointStyle: true } }, tooltip: { callbacks: { label: function(ctx) { if (ctx.dataset.label.indexOf('Patient') !== -1) return ctx.dataset.label + ': ' + ctx.parsed.y + ' mmHg'; return ctx.dataset.label + ': ' + ctx.parsed.y + ' mmHg at age ' + ctx.parsed.x; } } } } } }); } 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'); var bpc = document.getElementById('bp-chart-container'); if (bpc) bpc.classList.add('hidden'); }); // ═══════════════════════════════════════════════════════════ // BMI PERCENTILE — CDC 2000 // ═══════════════════════════════════════════════════════════ // CDC BMI-for-age LMS values load from /data/calculators/bmi-lms.json. // ═══════════════════════════════════════════════════════════ // CHART.JS GROWTH CURVE RENDERING // ═══════════════════════════════════════════════════════════ // 7-line reference set: 3, 10, 25, 50, 75, 90, 97. // Drops 5th and 95th (they crowded the 3rd/10th and 90th/97th labels // on mobile without adding clinical value — 3rd/97th are the // abnormal-threshold lines, the inner percentiles give the trend). var PERCENTILE_LINES = [ { p: 3, z: -1.88079, color: 'rgba(220,38,38,0.9)', dash: [6,4], width: 1.4 }, // red { p: 10, z: -1.28155, color: 'rgba(234,88,12,0.9)', dash: [], width: 1.2 }, // orange { p: 25, z: -0.67449, color: 'rgba(202,138,4,0.85)', dash: [], width: 1.1 }, // amber { p: 50, z: 0, color: 'rgba(22,163,74,0.95)', dash: [], width: 2.4 }, // green (center) { p: 75, z: 0.67449, color: 'rgba(37,99,235,0.85)', dash: [], width: 1.1 }, // blue { p: 90, z: 1.28155, color: 'rgba(124,58,237,0.9)', dash: [], width: 1.2 }, // violet { p: 97, z: 1.88079, color: 'rgba(219,39,119,0.9)', dash: [6,4], width: 1.4 } // pink ]; function generatePercentileCurves(lmsTable, percentileLines) { var ages = Object.keys(lmsTable).map(Number).sort(function(a,b){return a-b;}); var datasets = []; percentileLines.forEach(function(pl, idx) { var points = []; ages.forEach(function(age) { var lms = lmsTable[age]; var val = valueFromLMS(lms.L, lms.M, lms.S, pl.z); if (isFinite(val) && val > 0) points.push({ x: age, y: Math.round(val * 100) / 100 }); }); datasets.push({ label: pl.p + 'th', data: points, borderColor: pl.color, borderWidth: pl.width, borderDash: pl.dash, pointRadius: 0, pointHoverRadius: 0, fill: false, tension: 0.4, order: 10 }); }); // Fill bands between symmetric percentile pairs. // Indexes: 3=0, 10=1, 25=2, 50=3, 75=4, 90=5, 97=6 var fills = [ { outer: 0, inner: 6, color: 'rgba(220,38,38,0.04)' }, // 3rd-97th (widest) { outer: 1, inner: 5, color: 'rgba(234,88,12,0.04)' }, // 10th-90th { outer: 2, inner: 4, color: 'rgba(163,163,163,0.05)' } // 25th-75th (IQR) ]; fills.forEach(function(f) { if (datasets[f.outer] && datasets[f.inner]) { datasets[f.outer].fill = { target: f.inner, above: f.color, below: f.color }; } }); return datasets; } var _chartInstances = {}; // Chart.js plugin — labels each percentile curve at its right edge, // spreads labels bidirectionally so top (97/95/90) and bottom (3/5/10) // clusters both breathe, draws a thin leader from each label to the // actual curve position when nudged, then repaints the patient dot // ON TOP so it's never obscured by a label. var percentileLabelPlugin = { id: 'percentileLabels', afterDatasetsDraw: function(chart) { var ctx = chart.ctx; var font = '700 11px -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif'; var MIN_GAP = 15; // Collect percentile-line labels with native screen positions var items = []; chart.data.datasets.forEach(function(ds, i) { if (!ds.label || !/^\d+th$/.test(ds.label)) return; var meta = chart.getDatasetMeta(i); if (!meta || meta.hidden || !meta.data || !meta.data.length) return; var last = meta.data[meta.data.length - 1]; if (!last) return; items.push({ label: ds.label, color: ds.borderColor, x: last.x, nativeY: last.y, targetY: last.y }); }); // Top to bottom (ascending canvas Y) items.sort(function(a, b) { return a.nativeY - b.nativeY; }); // Forward pass — push items DOWN when crowded by item above for (var k = 1; k < items.length; k++) { if (items[k].targetY - items[k - 1].targetY < MIN_GAP) { items[k].targetY = items[k - 1].targetY + MIN_GAP; } } // Backward pass — pull items UP when still crowded by item below for (var j = items.length - 2; j >= 0; j--) { if (items[j + 1].targetY - items[j].targetY < MIN_GAP) { items[j].targetY = items[j + 1].targetY - MIN_GAP; } } // Clamp to chart area so nothing clips top/bottom var area = chart.chartArea; if (area) { items.forEach(function(it) { if (it.targetY < area.top + 6) it.targetY = area.top + 6; if (it.targetY > area.bottom - 6) it.targetY = area.bottom - 6; }); } // Draw leader + label items.forEach(function(it) { var stroke = typeof it.color === 'string' ? it.color : '#555'; var m = stroke.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (m) stroke = 'rgb(' + m[1] + ',' + m[2] + ',' + m[3] + ')'; ctx.save(); // Leader — only if the label was nudged away from its native Y if (Math.abs(it.targetY - it.nativeY) > 2) { ctx.strokeStyle = stroke; ctx.globalAlpha = 0.55; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(it.x, it.nativeY); ctx.lineTo(it.x + 4, it.targetY); ctx.stroke(); ctx.globalAlpha = 1; } // Label with white halo ctx.font = font; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(255,255,255,0.92)'; ctx.strokeText(it.label, it.x + 5, it.targetY); ctx.fillStyle = stroke; ctx.fillText(it.label, it.x + 5, it.targetY); ctx.restore(); }); // Repaint the patient dot ON TOP of the labels so it's never // covered by a nearby percentile label. chart.data.datasets.forEach(function(ds, i) { if (ds.label !== 'Patient') return; var meta = chart.getDatasetMeta(i); if (!meta || !meta.data || !meta.data.length) return; var pt = meta.data[0]; if (!pt) return; ctx.save(); ctx.beginPath(); ctx.arc(pt.x, pt.y, 5, 0, Math.PI * 2); ctx.fillStyle = '#2563eb'; ctx.fill(); ctx.lineWidth = 1.8; ctx.strokeStyle = '#ffffff'; ctx.stroke(); ctx.restore(); }); } }; function renderGrowthChart(canvasId, containerId, lmsTable, patientX, patientY, xLabel, yLabel, extraDatasets) { if (typeof Chart === 'undefined') return; // Chart.js not loaded yet var container = document.getElementById(containerId); if (!container) return; container.classList.remove('hidden'); if (_chartInstances[canvasId]) { _chartInstances[canvasId].destroy(); _chartInstances[canvasId] = null; } var datasets = generatePercentileCurves(lmsTable, PERCENTILE_LINES); // Patient data point — drawn on top of every curve. White ring so // it reads cleanly even when it lands directly on a colored line. datasets.push({ label: 'Patient', data: [{ x: patientX, y: patientY }], backgroundColor: '#2563eb', borderColor: '#ffffff', borderWidth: 1.8, pointRadius: 5, pointHoverRadius: 7, pointStyle: 'circle', showLine: false, type: 'scatter', order: -1 }); if (extraDatasets) extraDatasets.forEach(function(d) { datasets.push(d); }); _chartInstances[canvasId] = new Chart(document.getElementById(canvasId), { type: 'line', data: { datasets: datasets }, plugins: [percentileLabelPlugin], options: { responsive: true, maintainAspectRatio: true, aspectRatio: 1.6, animation: { duration: 300 }, // Leave ~28px of padding on the right so the percentile labels // ("50th", "95th", ...) drawn past the last data point are not // clipped by the canvas edge. layout: { padding: { right: 30 } }, scales: { x: { type: 'linear', title: { display: true, text: xLabel, font: { size: 12, weight: 'bold' } }, grid: { color: 'rgba(0,0,0,0.05)' } }, y: { title: { display: true, text: yLabel, font: { size: 12, weight: 'bold' } }, grid: { color: 'rgba(0,0,0,0.05)' } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(ctx) { if (ctx.dataset.label === 'Patient') return 'Patient: ' + ctx.parsed.y.toFixed(1) + ' ' + yLabel.split('(').pop().replace(')', ''); return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1); } } } } } }); } 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; var ageMo = parseInt(document.getElementById('bmi-age-mo').value) || 0; var age = ageYr + ageMo / 12; 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; } if (!BMI_LMS) { showToast('BMI percentile data is still loading.', 'info'); return; } 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) { extendedInfo = '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/Fenton growth LMS values load from /data/calculators/growth-lms.json. var wfaLMS = null; var cdcWfaLMS = null; var lfaLMS = null; var cdcLfaLMS = null; var hcfaLMS = null; var wflLMS = null; var fentonLMS = null; loadGrowthLmsData() .then(function(data) { wfaLMS = data.wfaLMS; cdcWfaLMS = data.cdcWfaLMS; lfaLMS = data.lfaLMS; cdcLfaLMS = data.cdcLfaLMS; hcfaLMS = data.hcfaLMS; wflLMS = data.wflLMS; fentonLMS = data.fentonLMS; }) .catch(function(err) { console.warn('[calculators] Growth LMS data unavailable:', err && err.message); }); // 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 || isWFL) ? '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 ? 'none' : ''; document.getElementById('growth-length-field').style.display = (currentGrowthType === 'wfa') ? 'none' : (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'); var chartContainer = document.getElementById('growth-chart-container'); if (chartContainer) chartContainer.classList.add('hidden'); if (_chartInstances['growth-chart-canvas']) { _chartInstances['growth-chart-canvas'].destroy(); _chartInstances['growth-chart-canvas'] = null; } // Show MPH section only for LFA var mphSection = document.getElementById('growth-mph-section'); if (mphSection) mphSection.style.display = (currentGrowthType === 'lfa') ? '' : 'none'; }); }); document.getElementById('btn-calc-growth').addEventListener('click', function() { var sex = document.getElementById('growth-sex').value; if (!sex) { showToast('Select sex', 'error'); return; } if (!wfaLMS || !cdcWfaLMS || !lfaLMS || !cdcLfaLMS || !hcfaLMS || !wflLMS || !fentonLMS) { showToast('Growth chart data is still loading.', 'info'); 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); renderGrowthChart('growth-chart-canvas', 'growth-chart-container', fentonLMS[sex], ga, wtGrams, 'Gestational Age (weeks)', 'Weight (g)'); return; } var yrEl = document.getElementById('growth-age-yr'); var moEl = document.getElementById('growth-age-mo'); var dyEl = document.getElementById('growth-age-d'); var yrRaw = yrEl ? yrEl.value.trim() : ''; var moRaw = moEl ? moEl.value.trim() : ''; var dyRaw = dyEl ? dyEl.value.trim() : ''; // Treat blank fields as 0, but require at least ONE field to have any value. // "0 days" (newborn at birth) is valid — explicit zero counts. if (yrRaw === '' && moRaw === '' && dyRaw === '') { showToast('Enter age (years, months, or days)', 'error'); return; } var yr = parseFloat(yrRaw) || 0; var mo = parseFloat(moRaw) || 0; var dy = parseFloat(dyRaw) || 0; var age = yr * 12 + mo + dy / 30.4375; // age in months, fractional OK if (age < 0) { showToast('Age cannot be negative', 'error'); return; } if (currentGrowthType === 'wfa') { var wt = parseFloat(document.getElementById('growth-weight').value); if (!wt) { showToast('Enter weight', 'error'); return; } // Use WHO for 0-60mo, CDC for >60mo var wfaTable = (age > 24) ? cdcWfaLMS[sex] : wfaLMS[sex]; var wfaChartTable = (age > 24) ? cdcWfaLMS[sex] : wfaLMS[sex]; var lms = interpolateLMS(wfaTable, age); var z = calcZFromLMS(wt, lms.L, lms.M, lms.S); var p = normalCDF(z) * 100; var src = (age > 24) ? 'CDC' : 'WHO'; resultDiv.classList.remove('hidden'); resultDiv.innerHTML = renderGrowthResult('Weight-for-Age (' + age + ' mo, ' + src + ')', wt, 'kg', z, p); if (age > 24) { var wfaYears = {}; Object.keys(wfaChartTable).forEach(function(k) { wfaYears[+(k/12).toFixed(2)] = wfaChartTable[k]; }); renderGrowthChart('growth-chart-canvas', 'growth-chart-container', wfaYears, +(age/12).toFixed(2), wt, 'Age (years)', 'Weight (kg)'); } else { renderGrowthChart('growth-chart-canvas', 'growth-chart-container', wfaChartTable, age, wt, 'Age (months)', 'Weight (kg)'); } } else if (currentGrowthType === 'lfa') { var len = parseFloat(document.getElementById('growth-length').value); if (!len) { showToast('Enter length/height', 'error'); return; } // Use WHO for 0-60mo, CDC for >60mo var lfaTable = (age > 24) ? cdcLfaLMS[sex] : lfaLMS[sex]; var lms = interpolateLMS(lfaTable, age); var z = calcZFromLMS(len, lms.L, lms.M, lms.S); var p = normalCDF(z) * 100; var src = (age > 24) ? 'CDC' : 'WHO'; resultDiv.classList.remove('hidden'); resultDiv.innerHTML = renderGrowthResult('Length/Height-for-Age (' + age + ' mo, ' + src + ')', len, 'cm', z, p); // Mid-parental height var mphDatasets = []; var motherHt = parseFloat((document.getElementById('growth-mother-ht') || {}).value); var fatherHt = parseFloat((document.getElementById('growth-father-ht') || {}).value); var mph = calcMidParentalHeight(motherHt, fatherHt, sex); if (mph) { // Calculate what percentile the MPH corresponds to using adult height LMS (~18y = 216mo) var adultLms = interpolateLMS(cdcLfaLMS[sex], 216); var mphZ = calcZFromLMS(mph.target, adultLms.L, adultLms.M, adultLms.S); var mphPctile = (Math.round(normalCDF(mphZ) * 10000) / 100).toFixed(2); resultDiv.innerHTML += 'GA ' + ga + ' weeks, ' + (risk === 'medium' ? 'with' : 'without') + ' neurotoxicity risk factors. AAP 2022 CPG (exact values from published nomograms). Always use clinical judgment.
'; // Chart: phototherapy + exchange threshold lines vs patient var chartDatasets = [ { label: 'Phototherapy Threshold', data: generateThresholdCurve(table), borderColor: '#ef4444', borderWidth: 2, borderDash: [], fill: { target: 'origin', above: 'rgba(239,68,68,0.08)' } } ]; if (exchangeTable) { chartDatasets.push({ label: 'Exchange Transfusion', data: generateThresholdCurve(exchangeTable), borderColor: '#7f1d1d', borderWidth: 2, borderDash: [6,3], fill: false }); } renderBiliChart(hours, tsb, chartDatasets); }); 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'); var bc = document.getElementById('bili-chart-container'); if (bc) bc.classList.add('hidden'); }); // Bhutani 1999 hour-specific risk nomogram — 40th / 75th / 95th // percentile TSB boundaries. Values are a pre-digitized transcription // of the original Figure 2 (Bhutani VK et al., Pediatrics 1999;103(1):6-14), // lifted verbatim from the JS data arrays used by the codingace.net // Bhutani calculator: // https://codingace.net/health/neonate_bilirubin_bhutani.html // Cross-checked against the AAP 2004 CPG reproduction of the same chart: // https://staffnet.southernhealth.ca/wp-content/uploads/Bhutani-Nomogram.pdf // (Reproduced with AAP permission from Pediatrics 2004;114:297-316) // 6-hour granularity through the first 72 h, 12-hour after. // // Zone semantics (per AAP 2004 reproduction legend): // High-risk: TSB >= 95th percentile // High-intermediate: 75th <= TSB < 95th // Low-intermediate: 40th <= TSB < 75th // Low-risk: TSB < 40th // // Clinical note: this nomogram is for RISK STRATIFICATION only. // For phototherapy/exchange decisions use the AAP 2022 tab // (which supersedes the 2004 treatment thresholds). var BHUTANI_ZONES = null; loadBhutaniZones() .then(function(data) { BHUTANI_ZONES = data; }) .catch(function(err) { console.warn('[calculators] Bhutani zone data unavailable:', err && err.message); }); 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; } if (!BHUTANI_ZONES) { showToast('Bhutani zone data is still loading.', 'info'); return; } var riskZone = classifyBhutaniRisk(BHUTANI_ZONES, hours, tsb); var resultDiv = document.getElementById('bili-result'); resultDiv.classList.remove('hidden'); resultDiv.innerHTML = 'Bhutani hour-specific risk nomogram for infants ≥ 35 weeks GA. Digitized values from Bhutani et al., Pediatrics 1999 (cross-checked against the AAP 2004 CPG reproduction). For phototherapy decisions use the AAP 2022 tab.
'; // Chart: Bhutani zone curves with patient renderBiliChart(hours, tsb, [ { label: '95th (High-Risk)', data: generateThresholdCurve(BHUTANI_ZONES.p95), borderColor: '#ef4444', borderWidth: 2, fill: false, borderDash: [] }, { label: '75th (High-Intermed)', data: generateThresholdCurve(BHUTANI_ZONES.p75), borderColor: '#f97316', borderWidth: 1.5, fill: { target: '-1', above: 'rgba(239,68,68,0.08)' }, borderDash: [3,3] }, { label: '40th (Low-Intermed)', data: generateThresholdCurve(BHUTANI_ZONES.p40), borderColor: '#f59e0b', borderWidth: 1.5, fill: { target: '-1', above: 'rgba(249,115,22,0.08)' }, borderDash: [3,3] } ]); }); 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'); var bc = document.getElementById('bili-chart-container'); if (bc) bc.classList.add('hidden'); }); // ═══════════════════════════════════════════════════════════ // VITAL SIGNS — Interactive age selector // Harriet Lane Handbook 23rd Edition reference values // ═══════════════════════════════════════════════════════════ var VITALS_DATA = {}; loadVitalsData() .then(function(data) { VITALS_DATA = data; }) .catch(function(err) { console.warn('[calculators] Vitals data unavailable:', err && err.message); }); // Use event delegation on the vitals panel so the listener works even // when the panel is hidden at init time (hidden elements are still in the DOM, // but a setTimeout-based approach can race with dynamic content loading). var vitalsPanel = document.getElementById('calc-vitals'); if (vitalsPanel) { vitalsPanel.addEventListener('change', function(e) { if (e.target.id !== 'vitals-age-select') return; var key = e.target.value; var resultDiv = document.getElementById('vitals-result'); if (!key || !VITALS_DATA[key]) { resultDiv.classList.add('hidden'); return; } var v = VITALS_DATA[key]; resultDiv.classList.remove('hidden'); resultDiv.innerHTML = renderVitalsResult(v); }); } // ═══════════════════════════════════════════════════════════ // RESUSCITATION MEDICATIONS — Harriet Lane Handbook / AHA PALS 2020 // ═══════════════════════════════════════════════════════════ var RESUS_DATA = null; loadResusMedsData() .then(function(data) { RESUS_DATA = data; }) .catch(function(err) { console.warn('[calculators] Resus meds data unavailable:', err && err.message); }); // Use event delegation for resus panel var resusPanel = document.getElementById('calc-resus'); if (resusPanel) { resusPanel.addEventListener('click', function(e) { var btn = e.target.closest('#btn-calc-resus, #btn-clear-resus'); if (!btn) return; if (btn.id === 'btn-clear-resus') { var wi = document.getElementById('resus-weight'); if (wi) wi.value = ''; var rd = document.getElementById('resus-result'); if (rd) { rd.classList.add('hidden'); rd.innerHTML = ''; } return; } // Calculate var weightEl = document.getElementById('resus-weight'); var resultDiv = document.getElementById('resus-result'); var w = parseFloat(weightEl.value); if (!w || w <= 0) { showToast('Please enter a valid weight.', 'error'); return; } if (!RESUS_DATA) { showToast('Resus medication data is still loading.', 'info'); return; } resultDiv.innerHTML = renderResusMeds(RESUS_DATA, w); resultDiv.classList.remove('hidden'); }); } // ═══════════════════════════════════════════════════════════ // GLASGOW COMA SCALE // ═══════════════════════════════════════════════════════════ // GCS sub-pill navigation var gcsPanel = document.getElementById('calc-gcs'); if (gcsPanel) { gcsPanel.addEventListener('click', function(e) { var pill = e.target.closest('[data-gcs]'); if (!pill) return; gcsPanel.querySelectorAll('[data-gcs]').forEach(function(p) { p.classList.remove('active'); }); pill.classList.add('active'); var isInfant = pill.dataset.gcs === 'infant'; var childP = document.getElementById('gcs-child-panel'); var infantP = document.getElementById('gcs-infant-panel'); if (childP) childP.style.display = isInfant ? 'none' : ''; if (infantP) infantP.style.display = isInfant ? '' : 'none'; updateGCS(); }); gcsPanel.addEventListener('change', function(e) { if (e.target.tagName === 'SELECT') updateGCS(); }); } function updateGCS() { var isInfant = document.querySelector('[data-gcs="infant"].active') !== null; var prefix = isInfant ? 'gcs-infant' : 'gcs-child'; var eye = parseInt(document.getElementById(prefix + '-eye').value) || 0; var verbal = parseInt(document.getElementById(prefix + '-verbal').value) || 0; var motor = parseInt(document.getElementById(prefix + '-motor').value) || 0; var score = calculateGcs(eye, verbal, motor); var resultDiv = document.getElementById('gcs-result'); if (resultDiv) { resultDiv.innerHTML = '