extract calculator vitals data

This commit is contained in:
Daniel 2026-05-08 03:06:14 +02:00
parent 0891894cc0
commit a4b03e8195
4 changed files with 231 additions and 149 deletions

View file

@ -0,0 +1,141 @@
{
"version": "1.0",
"source": "Harriet Lane Handbook 23rd Edition reference values",
"ageGroups": {
"premie": {
"label": "Premie",
"hr": { "awake": "120-170", "sleeping": "100-150" },
"rr": "40-70",
"sbp": "55-75",
"dbp": "35-45",
"temp": "36.5-37.5",
"weight": "0.5-2.5 kg",
"spo2": "88-95% (target)",
"notes": [
"HR and RR are highly variable and depend on gestational age",
"BP increases with gestational age and postnatal age",
"Target SpO2 88-95% to reduce retinopathy of prematurity risk",
"Temperature instability is common - use servo-controlled warmers",
"Bradycardia (<100 bpm) and apnea are common in premature infants"
]
},
"0-3mo": {
"label": "0-3 Months",
"hr": { "awake": "100-150", "sleeping": "85-135" },
"rr": "35-55",
"sbp": "65-85",
"dbp": "45-55",
"temp": "36.5-37.5",
"weight": "2.5-6 kg",
"spo2": ">95%",
"notes": [
"HR normally increases with crying (up to 180-190 bpm) - this is physiologic",
"Periodic breathing (pauses <10 sec) is normal in neonates",
"Acrocyanosis (blue hands/feet) is normal; central cyanosis is not",
"BP is best measured in the right arm (pre-ductal) in neonates",
"Normal weight loss of 5-7% in first 3-5 days; regain by 10-14 days"
]
},
"3-6mo": {
"label": "3-6 Months",
"hr": { "awake": "90-120", "sleeping": "75-110" },
"rr": "30-45",
"sbp": "70-90",
"dbp": "50-65",
"temp": "36.5-37.5",
"weight": "5-8 kg",
"spo2": ">95%",
"notes": [
"Expected weight gain: 20-30 g/day (150-200 g/week)",
"HR gradually decreases as vagal tone matures",
"RR >60 at rest may indicate lower respiratory tract disease",
"BP should be measured with appropriate cuff size (width 40% of arm circumference)"
]
},
"6-12mo": {
"label": "6-12 Months",
"hr": { "awake": "80-120", "sleeping": "70-110" },
"rr": "25-40",
"sbp": "80-100",
"dbp": "55-65",
"temp": "36.0-37.5",
"weight": "8-10 kg",
"spo2": ">95%",
"notes": [
"Expected weight: triple birth weight by 12 months (~10 kg average)",
"Weight gain slows to ~10-15 g/day",
"Sinus arrhythmia (HR varies with breathing) is normal",
"Febrile tachycardia: HR increases ~10 bpm per 1 degree C above 37"
]
},
"1-3yr": {
"label": "1-3 Years",
"hr": { "awake": "70-110", "sleeping": "60-100" },
"rr": "20-30",
"sbp": "90-105",
"dbp": "55-70",
"temp": "36.0-37.5",
"weight": "10-15 kg",
"spo2": ">95%",
"notes": [
"Expected weight gain: ~200-250 g/month (2-2.5 kg/year)",
"Tachycardia: HR >110 at rest warrants evaluation",
"Tachypnea: RR >30 at rest may indicate respiratory distress",
"BP screening begins at age 3 per AAP 2017 guidelines",
"Estimated weight: 2 x (age in years) + 8"
]
},
"3-6yr": {
"label": "3-6 Years",
"hr": { "awake": "65-110", "sleeping": "55-100" },
"rr": "20-25",
"sbp": "95-110",
"dbp": "60-75",
"temp": "36.0-37.5",
"weight": "14-20 kg",
"spo2": ">95%",
"notes": [
"Annual BP screening recommended from age 3",
"Normal BP <90th percentile for age, sex, and height",
"Elevated BP: 90th to <95th percentile (or 120/80 if lower)",
"Estimated weight: 2 x (age in years) + 8",
"ETT size (uncuffed): (age/4) + 4"
]
},
"6-12yr": {
"label": "6-12 Years",
"hr": { "awake": "60-95", "sleeping": "50-85" },
"rr": "14-22",
"sbp": "100-120",
"dbp": "60-75",
"temp": "36.0-37.5",
"weight": "20-40 kg",
"spo2": ">95%",
"notes": [
"Resting HR >95 or <60 warrants evaluation",
"BP should be measured at every clinical encounter",
"Stage 1 HTN: >=95th percentile on 3 separate occasions",
"Estimated weight: 3 x (age in years) + 7",
"ETT size (cuffed): (age/4) + 3.5"
]
},
">12yr": {
"label": ">12 Years",
"hr": { "awake": "55-85", "sleeping": "45-75" },
"rr": "12-18",
"sbp": "110-135",
"dbp": "65-85",
"temp": "36.0-37.5",
"weight": "40-80 kg",
"spo2": ">95%",
"notes": [
"Vital signs approach adult values",
"From age 13: use adult BP thresholds (AAP 2017)",
"Normal: <120/<80 mmHg; Elevated: 120-129/<80 mmHg",
"Stage 1 HTN: 130-139/80-89 mmHg; Stage 2 HTN: >=140/>=90 mmHg",
"Orthostatic vitals: measure lying, sitting, standing if dizzy",
"Athletic bradycardia (HR 45-60) may be normal in trained adolescents"
]
}
}
}

View file

@ -1,5 +1,6 @@
import { parseAgeMonths, formatParsedAge } from './calculators/age.js';
import { initImageLightbox } from './calculators/lightbox.js';
import { loadVitalsData, renderVitalsResult } from './calculators/vitals.js';
// ============================================================
// CALCULATORS.JS — Pediatric clinical calculators
@ -1662,135 +1663,10 @@ initImageLightbox();
// Harriet Lane Handbook 23rd Edition reference values
// ═══════════════════════════════════════════════════════════
var VITALS_DATA = {
premie: {
label: 'Premie',
hr: { awake: '120-170', sleeping: '100-150' },
rr: '40-70',
sbp: '55-75', dbp: '35-45',
temp: '36.5-37.5',
weight: '0.5-2.5 kg',
spo2: '88-95% (target)',
notes: [
'HR and RR are highly variable and depend on gestational age',
'BP increases with gestational age and postnatal age',
'Target SpO2 88-95% to reduce retinopathy of prematurity risk',
'Temperature instability is common — use servo-controlled warmers',
'Bradycardia (<100 bpm) and apnea are common in premature infants'
]
},
'0-3mo': {
label: '0-3 Months',
hr: { awake: '100-150', sleeping: '85-135' },
rr: '35-55',
sbp: '65-85', dbp: '45-55',
temp: '36.5-37.5',
weight: '2.5-6 kg',
spo2: '>95%',
notes: [
'HR normally increases with crying (up to 180-190 bpm) — this is physiologic',
'Periodic breathing (pauses <10 sec) is normal in neonates',
'Acrocyanosis (blue hands/feet) is normal; central cyanosis is not',
'BP is best measured in the right arm (pre-ductal) in neonates',
'Normal weight loss of 5-7% in first 3-5 days; regain by 10-14 days'
]
},
'3-6mo': {
label: '3-6 Months',
hr: { awake: '90-120', sleeping: '75-110' },
rr: '30-45',
sbp: '70-90', dbp: '50-65',
temp: '36.5-37.5',
weight: '5-8 kg',
spo2: '>95%',
notes: [
'Expected weight gain: 20-30 g/day (150-200 g/week)',
'HR gradually decreases as vagal tone matures',
'RR >60 at rest may indicate lower respiratory tract disease',
'BP should be measured with appropriate cuff size (width 40% of arm circumference)'
]
},
'6-12mo': {
label: '6-12 Months',
hr: { awake: '80-120', sleeping: '70-110' },
rr: '25-40',
sbp: '80-100', dbp: '55-65',
temp: '36.0-37.5',
weight: '8-10 kg',
spo2: '>95%',
notes: [
'Expected weight: triple birth weight by 12 months (~10 kg average)',
'Weight gain slows to ~10-15 g/day',
'Sinus arrhythmia (HR varies with breathing) is normal',
'Febrile tachycardia: HR increases ~10 bpm per 1 degree C above 37'
]
},
'1-3yr': {
label: '1-3 Years',
hr: { awake: '70-110', sleeping: '60-100' },
rr: '20-30',
sbp: '90-105', dbp: '55-70',
temp: '36.0-37.5',
weight: '10-15 kg',
spo2: '>95%',
notes: [
'Expected weight gain: ~200-250 g/month (2-2.5 kg/year)',
'Tachycardia: HR >110 at rest warrants evaluation',
'Tachypnea: RR >30 at rest may indicate respiratory distress',
'BP screening begins at age 3 per AAP 2017 guidelines',
'Estimated weight: 2 x (age in years) + 8'
]
},
'3-6yr': {
label: '3-6 Years',
hr: { awake: '65-110', sleeping: '55-100' },
rr: '20-25',
sbp: '95-110', dbp: '60-75',
temp: '36.0-37.5',
weight: '14-20 kg',
spo2: '>95%',
notes: [
'Annual BP screening recommended from age 3',
'Normal BP <90th percentile for age, sex, and height',
'Elevated BP: 90th to <95th percentile (or 120/80 if lower)',
'Estimated weight: 2 x (age in years) + 8',
'ETT size (uncuffed): (age/4) + 4'
]
},
'6-12yr': {
label: '6-12 Years',
hr: { awake: '60-95', sleeping: '50-85' },
rr: '14-22',
sbp: '100-120', dbp: '60-75',
temp: '36.0-37.5',
weight: '20-40 kg',
spo2: '>95%',
notes: [
'Resting HR >95 or <60 warrants evaluation',
'BP should be measured at every clinical encounter',
'Stage 1 HTN: >=95th percentile on 3 separate occasions',
'Estimated weight: 3 x (age in years) + 7',
'ETT size (cuffed): (age/4) + 3.5'
]
},
'>12yr': {
label: '>12 Years',
hr: { awake: '55-85', sleeping: '45-75' },
rr: '12-18',
sbp: '110-135', dbp: '65-85',
temp: '36.0-37.5',
weight: '40-80 kg',
spo2: '>95%',
notes: [
'Vital signs approach adult values',
'From age 13: use adult BP thresholds (AAP 2017)',
'Normal: <120/<80 mmHg; Elevated: 120-129/<80 mmHg',
'Stage 1 HTN: 130-139/80-89 mmHg; Stage 2 HTN: >=140/>=90 mmHg',
'Orthostatic vitals: measure lying, sitting, standing if dizzy',
'Athletic bradycardia (HR 45-60) may be normal in trained adolescents'
]
}
};
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,
@ -1805,26 +1681,7 @@ initImageLightbox();
var v = VITALS_DATA[key];
resultDiv.classList.remove('hidden');
resultDiv.innerHTML =
'<div class="calc-result-header" style="background:#eff6ff;border-color:#2563eb;">' +
'<div style="font-size:18px;font-weight:700;color:#1d4ed8;">' + v.label + '</div>' +
'</div>' +
'<div class="calc-result-grid" style="margin-top:12px;">' +
'<div class="calc-result-item"><div class="calc-result-label">Heart Rate (awake)</div><div class="calc-result-value" style="font-size:18px;color:#ef4444;">' + v.hr.awake + '</div><div class="calc-result-sub">bpm</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Heart Rate (sleep)</div><div class="calc-result-value" style="font-size:18px;color:#8b5cf6;">' + v.hr.sleeping + '</div><div class="calc-result-sub">bpm</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Respiratory Rate</div><div class="calc-result-value" style="font-size:18px;color:#0ea5e9;">' + v.rr + '</div><div class="calc-result-sub">breaths/min</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Systolic BP</div><div class="calc-result-value" style="font-size:18px;color:#f97316;">' + v.sbp + '</div><div class="calc-result-sub">mmHg</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Diastolic BP</div><div class="calc-result-value" style="font-size:18px;color:#f59e0b;">' + v.dbp + '</div><div class="calc-result-sub">mmHg</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Temperature</div><div class="calc-result-value" style="font-size:18px;color:#10b981;">' + v.temp + '</div><div class="calc-result-sub">&deg;C</div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">SpO2</div><div class="calc-result-value" style="font-size:18px;color:#06b6d4;">' + v.spo2 + '</div><div class="calc-result-sub"></div></div>' +
'<div class="calc-result-item"><div class="calc-result-label">Weight Range</div><div class="calc-result-value" style="font-size:18px;color:#6366f1;">' + v.weight + '</div><div class="calc-result-sub"></div></div>' +
'</div>' +
'<div style="margin-top:14px;padding:12px;background:var(--g50);border-radius:8px;">' +
'<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:6px;">Clinical Notes</div>' +
'<ul style="margin:0;padding-left:16px;font-size:12px;color:var(--g700);line-height:1.7;">' +
v.notes.map(function(n) { return '<li>' + n + '</li>'; }).join('') +
'</ul>' +
'</div>';
resultDiv.innerHTML = renderVitalsResult(v);
});
}

View file

@ -0,0 +1,37 @@
var vitalsCache = null;
export function loadVitalsData() {
if (vitalsCache) return Promise.resolve(vitalsCache);
return fetch('/data/calculators/vitals.json', { credentials: 'same-origin' })
.then(function(response) {
if (!response.ok) throw new Error('Failed to load vitals data');
return response.json();
})
.then(function(data) {
vitalsCache = data.ageGroups || {};
return vitalsCache;
});
}
export function renderVitalsResult(v) {
return ''
+ '<div class="calc-result-header" style="background:#eff6ff;border-color:#2563eb;">'
+ '<div style="font-size:18px;font-weight:700;color:#1d4ed8;">' + v.label + '</div>'
+ '</div>'
+ '<div class="calc-result-grid" style="margin-top:12px;">'
+ '<div class="calc-result-item"><div class="calc-result-label">Heart Rate (awake)</div><div class="calc-result-value" style="font-size:18px;color:#ef4444;">' + v.hr.awake + '</div><div class="calc-result-sub">bpm</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Heart Rate (sleep)</div><div class="calc-result-value" style="font-size:18px;color:#8b5cf6;">' + v.hr.sleeping + '</div><div class="calc-result-sub">bpm</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Respiratory Rate</div><div class="calc-result-value" style="font-size:18px;color:#0ea5e9;">' + v.rr + '</div><div class="calc-result-sub">breaths/min</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Systolic BP</div><div class="calc-result-value" style="font-size:18px;color:#f97316;">' + v.sbp + '</div><div class="calc-result-sub">mmHg</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Diastolic BP</div><div class="calc-result-value" style="font-size:18px;color:#f59e0b;">' + v.dbp + '</div><div class="calc-result-sub">mmHg</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Temperature</div><div class="calc-result-value" style="font-size:18px;color:#10b981;">' + v.temp + '</div><div class="calc-result-sub">&deg;C</div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">SpO2</div><div class="calc-result-value" style="font-size:18px;color:#06b6d4;">' + v.spo2 + '</div><div class="calc-result-sub"></div></div>'
+ '<div class="calc-result-item"><div class="calc-result-label">Weight Range</div><div class="calc-result-value" style="font-size:18px;color:#6366f1;">' + v.weight + '</div><div class="calc-result-sub"></div></div>'
+ '</div>'
+ '<div style="margin-top:14px;padding:12px;background:var(--g50);border-radius:8px;">'
+ '<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:6px;">Clinical Notes</div>'
+ '<ul style="margin:0;padding-left:16px;font-size:12px;color:var(--g700);line-height:1.7;">'
+ v.notes.map(function(n) { return '<li>' + n + '</li>'; }).join('')
+ '</ul>'
+ '</div>';
}

View file

@ -0,0 +1,47 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
function loadVitals() {
const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'calculators', 'vitals.json'), 'utf8');
return JSON.parse(raw);
}
test('vitals.json exposes all calculator age groups', () => {
const data = loadVitals();
assert.equal(data.version, '1.0');
assert.deepEqual(Object.keys(data.ageGroups), [
'premie',
'0-3mo',
'3-6mo',
'6-12mo',
'1-3yr',
'3-6yr',
'6-12yr',
'>12yr'
]);
});
test('vitals.json preserves key Harriet Lane ranges', () => {
const groups = loadVitals().ageGroups;
assert.equal(groups.premie.hr.awake, '120-170');
assert.equal(groups.premie.spo2, '88-95% (target)');
assert.equal(groups['0-3mo'].sbp, '65-85');
assert.equal(groups['3-6mo'].rr, '30-45');
assert.equal(groups['1-3yr'].weight, '10-15 kg');
assert.equal(groups['6-12yr'].hr.sleeping, '50-85');
assert.equal(groups['>12yr'].dbp, '65-85');
});
test('vitals.json entries have complete renderable fields', () => {
const groups = loadVitals().ageGroups;
Object.entries(groups).forEach(([key, group]) => {
assert.ok(group.label, `${key}.label`);
assert.ok(group.hr && group.hr.awake && group.hr.sleeping, `${key}.hr`);
['rr', 'sbp', 'dbp', 'temp', 'weight', 'spo2'].forEach((field) => {
assert.ok(group[field], `${key}.${field}`);
});
assert.ok(Array.isArray(group.notes) && group.notes.length > 0, `${key}.notes`);
});
});