bilirubin-calculator/src/bilirubin_app.js

244 lines
8.3 KiB
JavaScript

/*
Neonatal jaundice treatment threshold calculator
Copyright (C) 2024-2025 Lee Yingtong Li
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// --------------------------------------
// Extra questions by guideline/gestation
function updateExtraQuestions() {
// Show or hide extra questions according to guideline and gestation
let guideline = document.getElementById('guideline').value;
let gestation = document.getElementById('gestation').valueAsNumber;
if (guideline === 'rwh') {
document.getElementById('questions_rwh').classList.remove('hidden');
if (gestation >= 35) {
document.getElementById('questions_rwh_geq35').classList.remove('hidden');
document.getElementById('questions_rwh_lt35').classList.add('hidden');
} else {
document.getElementById('questions_rwh_geq35').classList.add('hidden');
document.getElementById('questions_rwh_lt35').classList.remove('hidden');
}
} else {
document.getElementById('questions_rwh').classList.add('hidden');
}
// Also update source in footer
if (guideline === 'nice') {
document.getElementById('guideline_source').innerHTML = 'National Institute for Health and Clinical Excellence. Neonatal jaundice treatment threshold graphs. In: Jaundice in newborn babies under 28 days. London: National Institute for Health and Clinical Excellence; 2023. (NICE clinical guidelines; CG98). <a href="https://www.nice.org.uk/guidance/cg98" class="text-blue-500 hover:underline hover:text-blue-600">https://www.nice.org.uk/guidance/cg98</a>';
} else if (guideline === 'rwh') {
if (gestation >= 35) {
document.getElementById('guideline_source').innerHTML = 'Royal Women\'s Hospital. Jaundice (hyperbilirubinaemia) in newborn infants ≥ 35 weeks gestation: guideline. Version 2.0. Melbourne: Royal Women\'s Hospital; 2025.';
} else {
document.getElementById('guideline_source').innerHTML = 'Royal Women\'s Hospital. Jaundice (hyperbilirubinaemia) in preterm infants &lt; 35 weeks gestation: guideline. Version 2.0. Melbourne: Royal Women\'s Hospital; 2025.';
}
} else {
throw new Error('Unexpected guideline');
}
}
updateExtraQuestions();
// --------------
// Graph plotting
function isGestationValid(guideline, gestation) {
if (isNaN(gestation)) {
return false;
}
if (guideline === 'nice' && gestation < 23) {
return false;
}
return true;
}
function plotGraphData() {
let guideline = document.getElementById('guideline').value;
let gestation = document.getElementById('gestation').valueAsNumber;
if (!isGestationValid(guideline, gestation)) {
chart.data.datasets[0].data = [];
chart.data.datasets[1].data = [];
} else if (guideline === 'nice') {
chart.data.datasets[0].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: nice_phototherapy_thresh(d, gestation)}));
chart.data.datasets[1].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: nice_exchange_thresh(d, gestation)}));
} else if (guideline === 'rwh') {
let dat = document.getElementById('dat').checked;
let bw_lt2500 = document.getElementById('bw_lt2500').checked;
let bw_lt1000 = document.getElementById('bw_lt1000').checked;
let fhx_rbc = document.getElementById('fhx_rbc').checked;
let bruising = document.getElementById('bruising').checked;
chart.data.datasets[0].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: rwh_phototherapy_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising)}));
chart.data.datasets[1].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: rwh_exchange_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising)}));
} else {
throw new Error('Unexpected guideline');
}
chart.update();
}
Chart.defaults.color = '#111827'; // gray-900
Chart.defaults.font.family = 'Manrope, Helvetica, Arial, sans-serif';
const chart = new Chart(document.getElementById('bilirubinChart'), {
type: 'scatter',
data: {
datasets: [
{
label: 'Phototherapy',
data: [],
showLine: true,
pointStyle: false
},
{
label: 'Exchange transfusion',
data: [],
showLine: true,
pointStyle: false
},
{
label: 'Measurement',
data: []
}
]
},
options: {
scales: {
x: {
title: {
display: true,
text: 'Chronological age (days)'
},
min: 0,
max: 14,
ticks: {
stepSize: 0.25,
callback: (value) => value % 1 == 0 ? value : '',
autoSkip: false,
maxRotation: 0
},
grid: {
color: (context) => context.tick.value % 1 == 0 ? '#9ca3af' : '#e5e7eb' // gray-400 / gray-200
}
},
y: {
title: {
display: true,
text: 'Total bilirubin (μmol/L)'
},
min: 0,
max: 550,
ticks: {
stepSize: 10,
callback: (value) => value % 50 == 0 ? value : '',
autoSkip: false
},
grid: {
color: (context) => context.tick.value % 50 == 0 ? '#9ca3af' : '#e5e7eb' // gray-400 / gray-200
}
}
},
interaction: {
mode: 'x',
intersect: false
},
plugins: {
tooltip: {
callbacks: {
title: (context) => prettyPrintDays(context[0].parsed.x),
label: (context) => context.dataset.label + ': ' + Math.round(context.parsed.y)
},
filter: (context, idx, items) => Math.abs(context.parsed.x - items[0].parsed.x) < 0.5/24
}
},
maintainAspectRatio: false
}
});
plotGraphData();
// ------------
// Prefill form
let dateNow = dateToISOStringLocal(new Date());
if (document.getElementById('time_birth').value === '') {
document.getElementById('time_birth').value = dateNow;
}
if (document.getElementById('time_measurement').value === '') {
document.getElementById('time_measurement').value = dateNow;
}
// --------------------------
// Plot bilirubin measurement
function updateBilirubin() {
let resultDiv = document.getElementById('result');
let resultP = resultDiv.querySelector('p');
let guideline = document.getElementById('guideline').value;
let bilirubin = document.getElementById('bilirubin').valueAsNumber;
let gestation = document.getElementById('gestation').valueAsNumber;
if (document.getElementById('time_birth').value === '') {
resultDiv.className = 'hidden';
return;
}
if (document.getElementById('time_measurement').value === '') {
resultDiv.className = 'hidden';
return;
}
if (isNaN(bilirubin)) {
resultDiv.className = 'hidden';
return;
}
if (!isGestationValid(guideline, gestation)) {
resultDiv.className = 'hidden';
return;
}
let d = chronoAgeInDays(new Date(document.getElementById('time_birth').value), new Date(document.getElementById('time_measurement').value));
// Calculate thresholds
let phototherapy_thresh_value, exchange_thresh_value;
if (guideline === 'nice') {
phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
exchange_thresh_value = nice_exchange_thresh(d, gestation);
} else if (guideline === 'rwh') {
let dat = document.getElementById('dat').checked;
let bw_lt2500 = document.getElementById('bw_lt2500').checked;
let bw_lt1000 = document.getElementById('bw_lt1000').checked;
let fhx_rbc = document.getElementById('fhx_rbc').checked;
let bruising = document.getElementById('bruising').checked;
phototherapy_thresh_value = rwh_phototherapy_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising);
exchange_thresh_value = rwh_exchange_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising);
} else {
throw new Error('Unexpected guideline');
}
// Format for display
let [result_text, text_colour, background_colour, accent_colour] = describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value);
resultDiv.className = 'border-l-4 ' + accent_colour + ' ' + background_colour + ' py-2 px-4 mt-4';
resultP.className = 'text-sm ' + text_colour;
resultP.innerHTML = result_text;
// Update graph
chart.data.datasets[2].data = [{x: d, y: bilirubin}];
chart.update();
}
updateBilirubin();