2024-08-26 17:36:16 +10:00
<!DOCTYPE html>
< html >
< head >
< meta charset = "utf-8" >
<!--
Neonatal jaundice treatment threshold calculator
Copyright (C) 2024 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 / > .
-->
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Neonatal jaundice treatment threshold calculator< / title >
< link href = "build/main.css" rel = "stylesheet" >
< link href = "https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap" rel = "stylesheet" >
< script src = "https://cdn.jsdelivr.net/npm/chart.js" > < / script >
< / head >
< body class = "w-full" >
< div class = "max-w-2xl mx-auto mt-4" >
< h1 class = "text-gray-900 font-medium text-xl pb-3 mb-4 border-b border-gray-400" > Neonatal jaundice treatment thresholds< / h1 >
< div class = "space-y-2" >
< div class = "sm:grid sm:grid-cols-3" >
< label for = "gestation" class = "text-sm font-medium text-gray-900 pt-1.5" > Gestational age:< / label >
< div class = "col-span-2 relative rounded-md shadow-sm" >
< input id = "gestation" type = "number" value = "38" min = "22" onchange = "plotGraphData();updateBilirubin()" class = "w-full rounded-md border-0 py-1.5 pr-[8.8rem] text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 text-sm" >
< div class = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" >
< span class = "text-gray-500 text-sm" > completed weeks< / span >
< / div >
< / div >
< / div >
< div class = "sm:grid sm:grid-cols-3" >
< label for = "time_birth" class = "text-sm font-medium text-gray-900 pt-1.5" > Time of birth:< / label >
< input id = "time_birth" type = "datetime-local" onchange = "updateBilirubin()" class = "col-span-2 w-full rounded-md shadow-sm border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 text-sm" >
< / div >
< div class = "sm:grid sm:grid-cols-3" >
< label for = "time_measurement" class = "text-sm font-medium text-gray-900 pt-1.5" > Time of measurement:< / label >
< input id = "time_measurement" type = "datetime-local" onchange = "updateBilirubin()" class = "col-span-2 w-full rounded-md shadow-sm border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 text-sm" >
< / div >
< div class = "sm:grid sm:grid-cols-3" >
< label for = "bilirubin" class = "text-sm font-medium text-gray-900 pt-1.5" > Total bilirubin:< / label >
< div class = "col-span-2 relative rounded-md shadow-sm" >
< input id = "bilirubin" type = "number" oninput = "updateBilirubin()" class = "w-full rounded-md border-0 py-1.5 pr-[4.5rem] text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 text-sm" >
< div class = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" >
< span class = "text-gray-500 text-sm" > μmol/L< / span >
< / div >
< / div >
< / div >
2024-08-26 18:45:07 +10:00
< div id = "result" class = "hidden" >
2024-08-26 17:36:16 +10:00
< p > < / p >
< / div >
< / div >
< div class = "border-t border-gray-400 mt-4 pt-2" >
< canvas id = "bilirubinChart" width = "672" height = "336" > < / canvas >
< / div >
< div class = "border-t border-gray-400 mt-4 pt-4 text-xs text-gray-600 space-y-2 mb-4" >
< p > Treatment thresholds as per: 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 > < / p >
< p > This tool is made available in the hope that it will be useful, but < em > WITHOUT ANY WARRANTY< / em > ; without even the implied warranty of < em > MERCHANTABILITY< / em > or < em > FITNESS FOR A PARTICULAR PURPOSE< / em > . Information provided in this tool is intended for reference by medical professionals. Nothing in this tool is intended to constitute medical advice.< / p >
< p > Lee Yingtong Li, 2024. Source code available at < a href = "https://yingtongli.me/git/bilirubin-calculator" class = "text-blue-500 hover:underline hover:text-blue-600" > https://yingtongli.me/git/bilirubin-calculator< / a > .< / p >
< / div >
< / div >
< script >
// ----------------------
// Threshold calculations
function phototherapy_38wks(d) {
if (d < = 0) {
return 100;
}
if (d < = 1) {
return d * 100 + 100;
}
if (d < = 4) {
return (d - 1) * 150 / 3 + 200;
}
return 350;
}
function exchange_38wks(d) {
if (d < = 0) {
return 100;
}
if (d < = 1.75) {
return d * 350 / 1.75 + 100;
}
return 450;
}
function phototherapy_thresh(d, gestation) {
if (gestation >= 38) {
return phototherapy_38wks(d);
}
if (gestation < 22 ) {
2024-08-26 18:45:07 +10:00
throw new Error('Invalid gestation');
2024-08-26 17:36:16 +10:00
}
if (d < = 0) {
return 40;
}
let thresh_at_3days = gestation * 10 - 100;
if (d < = 3) {
return d * (thresh_at_3days - 40) / 3 + 40;
}
return thresh_at_3days;
}
function exchange_thresh(d, gestation) {
if (gestation >= 38) {
return exchange_38wks(d);
}
if (gestation < 22 ) {
2024-08-26 18:45:07 +10:00
throw new Error('Invalid gestation');
2024-08-26 17:36:16 +10:00
}
if (d < = 0) {
return 80;
}
let thresh_at_3days = gestation * 10;
if (d < = 3) {
return d * (thresh_at_3days - 80) / 3 + 80;
}
return thresh_at_3days;
}
// --------------
// Graph plotting
function plotGraphData() {
let gestation = document.getElementById('gestation').valueAsNumber;
2024-08-26 18:45:07 +10:00
if (isNaN(gestation) || gestation < 22 ) {
chart.data.datasets[0].data = [];
chart.data.datasets[1].data = [];
} else {
chart.data.datasets[0].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: phototherapy_thresh(d, gestation)}));
chart.data.datasets[1].data = [...Array(14*24).keys()].map((i) => i / 24).map((d) => ({x: d, y: exchange_thresh(d, gestation)}));
}
2024-08-26 17:36:16 +10:00
chart.update();
}
function prettyPrintDays(d) {
if (d < 1 ) {
return (d * 24).toFixed(0) + ' hours';
}
if (d < 2 ) {
return '1 day, ' + ((d % 1) * 24).toFixed(0) + ' hours';
}
return Math.floor(d) + ' days, ' + ((d % 1) * 24).toFixed(0) + ' hours';
}
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: 'index',
intersect: false
},
plugins: {
tooltip: {
callbacks: {
title: (context) => prettyPrintDays(context[0].parsed.x),
label: (context) => context.dataset.label + ': ' + Math.round(context.parsed.y)
}
}
},
maintainAspectRatio: false
}
});
plotGraphData();
// ------------
// Prefill form
2024-08-26 18:36:59 +10:00
function dateToISOStringLocal(date) {
function pad(n) {
if (n < 10 ) {
return '0' + n;
}
return '' + n;
}
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
2024-08-26 17:36:16 +10:00
2024-08-26 18:36:59 +10:00
let dateNow = dateToISOStringLocal(new Date());
2024-08-26 17:36:16 +10:00
if (document.getElementById('time_birth').value === '') {
2024-08-26 18:36:59 +10:00
document.getElementById('time_birth').value = dateNow;
2024-08-26 17:36:16 +10:00
}
if (document.getElementById('time_measurement').value === '') {
2024-08-26 18:36:59 +10:00
document.getElementById('time_measurement').value = dateNow;
2024-08-26 17:36:16 +10:00
}
// --------------------------
// Plot bilirubin measurement
function prettyPrintBilirubin(b) {
if (b < 10 ) {
return b.toFixed(1);
} else {
return Math.round(b).toFixed(0);
}
}
function updateBilirubin() {
2024-08-26 18:45:07 +10:00
let resultDiv = document.getElementById('result');
let resultP = resultDiv.querySelector('p');
let bilirubin = document.getElementById('bilirubin').valueAsNumber;
let gestation = document.getElementById('gestation').valueAsNumber;
2024-08-26 17:36:16 +10:00
if (document.getElementById('time_birth').valueAsDate === null) {
2024-08-26 18:45:07 +10:00
resultDiv.className = 'hidden';
2024-08-26 17:36:16 +10:00
return;
}
if (document.getElementById('time_measurement').valueAsDate === null) {
2024-08-26 18:45:07 +10:00
resultDiv.className = 'hidden';
return;
}
if (isNaN(bilirubin)) {
resultDiv.className = 'hidden';
2024-08-26 17:36:16 +10:00
return;
}
2024-08-26 18:45:07 +10:00
if (isNaN(gestation) || gestation < 22 ) {
resultDiv.className = 'hidden';
2024-08-26 17:36:16 +10:00
return;
}
// Chronological age in days
let d = (document.getElementById('time_measurement').valueAsDate.getTime() - document.getElementById('time_birth').valueAsDate.getTime()) / 1000 / 60 / 60 / 24;
let phototherapy_thresh_value = phototherapy_thresh(d, gestation);
let exchange_thresh_value = exchange_thresh(d, gestation);
chart.data.datasets[2].data = [{x: d, y: bilirubin}];
chart.update();
if (bilirubin < phototherapy_thresh_value ) {
if (bilirubin < phototherapy_thresh_value - 50 ) {
resultDiv.className = 'border-l-4 border-sky-400 bg-sky-50 py-2 px-4';
resultP.className = 'text-sm text-sky-800';
} else {
resultDiv.className = 'border-l-4 border-yellow-400 bg-yellow-50 py-2 px-4';
resultP.className = 'text-sm text-yellow-800';
}
resultP.innerHTML = 'Bilirubin of ' + bilirubin + ' at ' + prettyPrintDays(d) + ' is < span class = "font-bold" > ' + prettyPrintBilirubin(phototherapy_thresh_value - bilirubin) + ' below< / span > the phototherapy threshold.';
} else if (bilirubin < exchange_thresh_value ) {
resultDiv.className = 'border-l-4 border-orange-400 bg-orange-50 py-2 px-4';
resultP.className = 'text-sm text-orange-800';
resultP.innerHTML = 'Bilirubin of ' + bilirubin + ' at ' + prettyPrintDays(d) + ' is < span class = "font-bold" > ' + prettyPrintBilirubin(bilirubin - phototherapy_thresh_value) + ' above< / span > the phototherapy threshold, ' + prettyPrintBilirubin(exchange_thresh_value - bilirubin) + ' below the exchange transfusion threshold.';
} else {
resultDiv.className = 'border-l-4 border-red-400 bg-red-50 py-2 px-4';
resultP.className = 'text-sm text-red-800';
resultP.innerHTML = 'Bilirubin of ' + bilirubin + ' at ' + prettyPrintDays(d) + ' is ' + prettyPrintBilirubin(bilirubin - exchange_thresh_value) + ' above the < span class = "font-bold" > exchange transfusion< / span > threshold.';
}
}
updateBilirubin();
< / script >
< / body >
< / html >