consolidate calculator growth helpers
This commit is contained in:
parent
15c74cba0a
commit
acbd42aa43
3 changed files with 39 additions and 59 deletions
|
|
@ -5,7 +5,7 @@ import { calculateBPAssessment, computeBPPercentile, loadBpReferenceData } from
|
|||
import { calculateBsa, calculateWeightBasedDose } from './calculators/dosing.js';
|
||||
import { loadEquipmentData, renderEquipmentResult } from './calculators/equipment.js';
|
||||
import { calculateGcs } from './calculators/gcs.js';
|
||||
import { loadGrowthLmsData } from './calculators/growth.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';
|
||||
|
|
@ -194,25 +194,10 @@ initImageLightbox();
|
|||
|
||||
// CDC BMI-for-age LMS values load from /data/calculators/bmi-lms.json.
|
||||
|
||||
function normalCDF(z) {
|
||||
var a1=0.254829592, a2=-0.284496736, a3=1.421413741, a4=-1.453152027, a5=1.061405429, p=0.3275911;
|
||||
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);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// CHART.JS GROWTH CURVE RENDERING
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// Inverse LMS: given Z-score, return measurement value
|
||||
function valueFromLMS(L, M, S, z) {
|
||||
if (Math.abs(L) < 0.001) return M * Math.exp(S * z);
|
||||
return M * Math.pow(1 + L * S * z, 1 / L);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -431,13 +416,6 @@ initImageLightbox();
|
|||
});
|
||||
}
|
||||
|
||||
// Mid-parental height calculation
|
||||
function calcMidParentalHeight(motherHt, fatherHt, sex) {
|
||||
if (!motherHt || !fatherHt) return null;
|
||||
var mph = sex === 'male' ? (motherHt + fatherHt + 13) / 2 : (motherHt + fatherHt - 13) / 2;
|
||||
return { target: mph, low: mph - 8.5, high: mph + 8.5 };
|
||||
}
|
||||
|
||||
var BMI_LMS = null;
|
||||
loadBmiLmsData()
|
||||
.then(function(data) { BMI_LMS = data; })
|
||||
|
|
@ -620,42 +598,6 @@ initImageLightbox();
|
|||
});
|
||||
});
|
||||
|
||||
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 * 100) / 100).toFixed(2);
|
||||
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; }
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ export function normalCDF(z) {
|
|||
return 0.5 * (1 + sign * y);
|
||||
}
|
||||
|
||||
export function valueFromLMS(L, M, S, z) {
|
||||
if (Math.abs(L) < 0.001) return M * Math.exp(S * z);
|
||||
return M * Math.pow(1 + L * S * z, 1 / L);
|
||||
}
|
||||
|
||||
export function 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);
|
||||
|
|
@ -46,3 +51,26 @@ export function calculateLmsPercentile(lmsTable, x, value) {
|
|||
var z = calcZFromLMS(value, lms.L, lms.M, lms.S);
|
||||
return { lms: lms, z: z, percentile: normalCDF(z) * 100 };
|
||||
}
|
||||
|
||||
export function calcMidParentalHeight(motherHt, fatherHt, sex) {
|
||||
if (!motherHt || !fatherHt) return null;
|
||||
var mph = sex === 'male' ? (motherHt + fatherHt + 13) / 2 : (motherHt + fatherHt - 13) / 2;
|
||||
return { target: mph, low: mph - 8.5, high: mph + 8.5 };
|
||||
}
|
||||
|
||||
export function renderGrowthResult(label, value, unit, z, percentile) {
|
||||
var pctl = (Math.round(percentile * 100) / 100).toFixed(2);
|
||||
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>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,16 @@ test('growth calculator preserves legacy outputs across chart combinations', asy
|
|||
});
|
||||
});
|
||||
|
||||
test('growth helper exports preserve mid-parental height and result rendering', async () => {
|
||||
const growth = await import('../public/js/calculators/growth.js');
|
||||
assert.deepEqual(growth.calcMidParentalHeight(160, 180, 'male'), { target: 176.5, low: 168, high: 185 });
|
||||
assert.deepEqual(growth.calcMidParentalHeight(160, 180, 'female'), { target: 163.5, low: 155, high: 172 });
|
||||
assert.equal(Number(growth.valueFromLMS(1, 75, 0.03, 0).toFixed(2)), 75);
|
||||
const html = growth.renderGrowthResult('Weight-for-Age', 8, 'kg', 0.07, 52.79);
|
||||
assert.match(html, /52\.79<span style="font-size:12px;">th/);
|
||||
assert.match(html, /Weight-for-Age: 8 kg/);
|
||||
});
|
||||
|
||||
test('bp-reference.json exposes complete height LMS and coefficient tables', () => {
|
||||
const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'bp-reference.json'), 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
|
|
|
|||
Loading…
Reference in a new issue