Growth chart: flexible age input with smart parser
Replace [years] + [months dropdown] with a single text field that
accepts:
3y / 3 years / 3 yr
29m / 29 months / 29 mo
2y5m / 2 years 5 months / 2 yr 5 mo
3.5 years / 36 (plain number = months)
15 days / 2 weeks / 3y 2m 10d
Enables fractional ages so newborns can be plotted accurately
(WHO/CDC growth curves are continuous — "0 months" means at birth,
not a 0-27 day bucket, so a 15-day-old should plot at ~0.5 months).
Live hint below the field shows how the input was interpreted
("= 2 yr 5 mo (29 mo total)").
This commit is contained in:
parent
3e05d8eec9
commit
42984e355b
2 changed files with 67 additions and 14 deletions
|
|
@ -216,14 +216,8 @@
|
|||
</div>
|
||||
<div class="calc-field" id="growth-age-field">
|
||||
<label>Age</label>
|
||||
<div style="display:flex;gap:4px;align-items:center;">
|
||||
<input type="number" id="growth-age-yr" min="0" max="20" step="1" placeholder="yr" style="width:40%;">
|
||||
<select id="growth-age-mo" style="width:55%;padding:8px 4px;border:1.5px solid var(--g300);border-radius:6px;font-size:13px;">
|
||||
<option value="0">0 mo</option><option value="1">1 mo</option><option value="2">2 mo</option><option value="3">3 mo</option>
|
||||
<option value="4">4 mo</option><option value="5">5 mo</option><option value="6">6 mo</option><option value="7">7 mo</option>
|
||||
<option value="8">8 mo</option><option value="9">9 mo</option><option value="10">10 mo</option><option value="11">11 mo</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" id="growth-age" placeholder="e.g. 2y 5m, 29 months, 3 years, 15 days" autocomplete="off">
|
||||
<div id="growth-age-parsed" style="font-size:11px;color:var(--g500);margin-top:4px;min-height:14px;"></div>
|
||||
</div>
|
||||
<div class="calc-field" id="growth-ga-field" style="display:none;">
|
||||
<label>Gestational Age (weeks)</label>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,52 @@
|
|||
initCalculators();
|
||||
});
|
||||
|
||||
// ── Flexible age parser ─────────────────────────────────────────────
|
||||
// Accepts: "3y", "3 years", "29m", "29 months", "2y5m", "2 years 5 months",
|
||||
// "15d", "15 days", "3y 2m 10d", "36" (months), "3.5 years", etc.
|
||||
// Returns age in months (may be fractional), or null if unparseable.
|
||||
function parseAgeMonths(input) {
|
||||
if (input == null) return null;
|
||||
var s = String(input).toLowerCase().trim();
|
||||
if (!s) return null;
|
||||
// Plain number with no unit: if >= 24 assume years, else months. Matches
|
||||
// common shorthand ("36" for 3y, "6" for 6 months).
|
||||
if (/^[\d.]+$/.test(s)) {
|
||||
var n = parseFloat(s);
|
||||
if (isNaN(n)) return null;
|
||||
return n >= 24 ? n : n; // keep as months; user can add unit if they meant years
|
||||
}
|
||||
var total = 0;
|
||||
var matched = false;
|
||||
// Use (?![a-z]) instead of \b so "2y5m" terminates "y" correctly.
|
||||
// Order matters: longest-first alternations (years before yrs before y, etc.).
|
||||
var my = s.match(/([\d.]+)\s*(?:years|year|yrs|yr|y)(?![a-z])/);
|
||||
if (my) { total += parseFloat(my[1]) * 12; matched = true; }
|
||||
var mm = s.match(/([\d.]+)\s*(?:months|month|mos|mo|m)(?![a-z])/);
|
||||
if (mm) { total += parseFloat(mm[1]); matched = true; }
|
||||
var mw = s.match(/([\d.]+)\s*(?:weeks|week|wks|wk|w)(?![a-z])/);
|
||||
if (mw) { total += parseFloat(mw[1]) * 7 / 30.4375; matched = true; }
|
||||
var md = s.match(/([\d.]+)\s*(?:days|day|d)(?![a-z])/);
|
||||
if (md) { total += parseFloat(md[1]) / 30.4375; matched = true; }
|
||||
return matched ? total : null;
|
||||
}
|
||||
|
||||
function formatParsedAge(months) {
|
||||
if (months < 1) {
|
||||
var d = Math.round(months * 30.4375);
|
||||
return '= ' + d + ' day' + (d === 1 ? '' : 's') + ' (' + months.toFixed(2) + ' mo)';
|
||||
}
|
||||
if (months < 24) {
|
||||
return '= ' + (Math.round(months * 10) / 10) + ' months';
|
||||
}
|
||||
var yr = Math.floor(months / 12);
|
||||
var mo = Math.round(months - yr * 12);
|
||||
if (mo === 12) { yr += 1; mo = 0; }
|
||||
return '= ' + yr + ' yr' + (mo ? ' ' + mo + ' mo' : '') + ' (' + Math.round(months) + ' mo total)';
|
||||
}
|
||||
// Expose for debugging / other modules
|
||||
window._parseAgeMonths = parseAgeMonths;
|
||||
|
||||
function initCalculators() {
|
||||
// ── Top-level pill navigation (scoped to .calc-nav-pill only) ──
|
||||
document.querySelectorAll('.calc-nav-pill').forEach(function(pill) {
|
||||
|
|
@ -1105,10 +1151,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
var growthYr = parseFloat(document.getElementById('growth-age-yr').value) || 0;
|
||||
var growthMo = parseInt(document.getElementById('growth-age-mo').value) || 0;
|
||||
var age = growthYr * 12 + growthMo; // age in months
|
||||
if (isNaN(age)) { showToast('Enter age in months', 'error'); return; }
|
||||
var rawAge = (document.getElementById('growth-age').value || '').trim();
|
||||
var age = parseAgeMonths(rawAge); // age in months, may be fractional
|
||||
if (age == null || isNaN(age) || age < 0) { showToast('Enter age (e.g. 2y 5m, 29 months, 15 days)', 'error'); return; }
|
||||
|
||||
if (currentGrowthType === 'wfa') {
|
||||
var wt = parseFloat(document.getElementById('growth-weight').value);
|
||||
|
|
@ -1207,13 +1252,27 @@
|
|||
});
|
||||
|
||||
document.getElementById('btn-clear-growth').addEventListener('click', function() {
|
||||
['growth-sex', 'growth-age-yr', 'growth-ga', 'growth-weight', 'growth-length', 'growth-hc'].forEach(function(id) {
|
||||
['growth-sex', 'growth-age', 'growth-ga', 'growth-weight', 'growth-length', 'growth-hc'].forEach(function(id) {
|
||||
var el = document.getElementById(id); if (el) el.value = '';
|
||||
});
|
||||
var gaMo = document.getElementById('growth-age-mo'); if (gaMo) gaMo.value = '0';
|
||||
var hint = document.getElementById('growth-age-parsed'); if (hint) hint.textContent = '';
|
||||
document.getElementById('growth-result').classList.add('hidden');
|
||||
});
|
||||
|
||||
// Live feedback as user types the age
|
||||
var ageEl = document.getElementById('growth-age');
|
||||
var ageHint = document.getElementById('growth-age-parsed');
|
||||
if (ageEl && ageHint) {
|
||||
ageEl.addEventListener('input', function() {
|
||||
var v = ageEl.value.trim();
|
||||
if (!v) { ageHint.textContent = ''; return; }
|
||||
var m = parseAgeMonths(v);
|
||||
if (m == null || isNaN(m) || m < 0) { ageHint.textContent = 'Unrecognized format'; ageHint.style.color = '#ef4444'; return; }
|
||||
ageHint.style.color = 'var(--g500)';
|
||||
ageHint.textContent = formatParsedAge(m);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// BILIRUBIN — AAP 2022 Phototherapy + Bhutani Nomogram
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Reference in a new issue