Refactor code into lib and app
This commit is contained in:
parent
de27115917
commit
f8e26d4e4d
|
@ -17,4 +17,4 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# Pass -w to watch continuously
|
# Pass -w to watch continuously
|
||||||
tailwindcss -c tailwind.config.js -i main.css -o build/main.css $@
|
tailwindcss -c tailwind.config.js -i src/main.css -o build/main.css $@
|
||||||
|
|
332
index.html
332
index.html
|
@ -1,332 +0,0 @@
|
||||||
|
|
||||||
<!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 my-4 px-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>
|
|
||||||
<div id="result" class="hidden">
|
|
||||||
<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">
|
|
||||||
<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) {
|
|
||||||
throw new Error('Invalid gestation');
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
throw new Error('Invalid gestation');
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
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)}));
|
|
||||||
}
|
|
||||||
|
|
||||||
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: '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
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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 prettyPrintBilirubin(b) {
|
|
||||||
if (b < 10) {
|
|
||||||
return b.toFixed(1);
|
|
||||||
} else {
|
|
||||||
return Math.round(b).toFixed(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBilirubin() {
|
|
||||||
let resultDiv = document.getElementById('result');
|
|
||||||
let resultP = resultDiv.querySelector('p');
|
|
||||||
|
|
||||||
let bilirubin = document.getElementById('bilirubin').valueAsNumber;
|
|
||||||
let gestation = document.getElementById('gestation').valueAsNumber;
|
|
||||||
|
|
||||||
if (document.getElementById('time_birth').valueAsDate === null) {
|
|
||||||
resultDiv.className = 'hidden';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (document.getElementById('time_measurement').valueAsDate === null) {
|
|
||||||
resultDiv.className = 'hidden';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isNaN(bilirubin)) {
|
|
||||||
resultDiv.className = 'hidden';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isNaN(gestation) || gestation < 22) {
|
|
||||||
resultDiv.className = 'hidden';
|
|
||||||
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>
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
// Graph plotting
|
||||||
|
|
||||||
|
function plotGraphData() {
|
||||||
|
let gestation = document.getElementById('gestation').valueAsNumber;
|
||||||
|
|
||||||
|
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)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bilirubin = document.getElementById('bilirubin').valueAsNumber;
|
||||||
|
let gestation = document.getElementById('gestation').valueAsNumber;
|
||||||
|
|
||||||
|
if (document.getElementById('time_birth').valueAsDate === null) {
|
||||||
|
resultDiv.className = 'hidden';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.getElementById('time_measurement').valueAsDate === null) {
|
||||||
|
resultDiv.className = 'hidden';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(bilirubin)) {
|
||||||
|
resultDiv.className = 'hidden';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNaN(gestation) || gestation < 22) {
|
||||||
|
resultDiv.className = 'hidden';
|
||||||
|
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();
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// 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) {
|
||||||
|
throw new Error('Invalid gestation');
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
throw new Error('Invalid gestation');
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
// Utility functions
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyPrintBilirubin(b) {
|
||||||
|
if (b < 10) {
|
||||||
|
return b.toFixed(1);
|
||||||
|
} else {
|
||||||
|
return Math.round(b).toFixed(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
|
||||||
|
<!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 my-4 px-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>
|
||||||
|
<div id="result" class="hidden">
|
||||||
|
<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">
|
||||||
|
<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 src="bilirubin_lib.js"></script>
|
||||||
|
<script src="bilirubin_app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -19,7 +19,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./*.html'],
|
content: ['./src/*.html', './src/*.js'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|
Loading…
Reference in New Issue