diff --git a/build_css.sh b/build_css.sh index 2941036..adc6a5f 100755 --- a/build_css.sh +++ b/build_css.sh @@ -17,4 +17,4 @@ # along with this program. If not, see . # 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 $@ diff --git a/index.html b/index.html deleted file mode 100644 index 87ef633..0000000 --- a/index.html +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - - - - - Neonatal jaundice treatment threshold calculator - - - - - -
-

Neonatal jaundice treatment thresholds

-
-
- -
- -
- completed weeks -
-
-
-
- - -
-
- - -
-
- -
- -
- μmol/L -
-
-
- -
-
- -
-
-

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). https://www.nice.org.uk/guidance/cg98

-

This tool is made available 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. Information provided in this tool is intended for reference by medical professionals. Nothing in this tool is intended to constitute medical advice.

-

Lee Yingtong Li, 2024. Source code available at https://yingtongli.me/git/bilirubin-calculator.

-
-
- - - diff --git a/src/bilirubin_app.js b/src/bilirubin_app.js new file mode 100644 index 0000000..5e83c3b --- /dev/null +++ b/src/bilirubin_app.js @@ -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 . +*/ + +// -------------- +// 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 ' + prettyPrintBilirubin(phototherapy_thresh_value - bilirubin) + ' below 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 ' + prettyPrintBilirubin(bilirubin - phototherapy_thresh_value) + ' above 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 exchange transfusion threshold.'; + } +} +updateBilirubin(); diff --git a/src/bilirubin_lib.js b/src/bilirubin_lib.js new file mode 100644 index 0000000..4132177 --- /dev/null +++ b/src/bilirubin_lib.js @@ -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 . +*/ + +// ---------------------- +// 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()); +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..45a25d6 --- /dev/null +++ b/src/index.html @@ -0,0 +1,77 @@ + + + + + + + + + + Neonatal jaundice treatment threshold calculator + + + + + +
+

Neonatal jaundice treatment thresholds

+
+
+ +
+ +
+ completed weeks +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+ μmol/L +
+
+
+ +
+
+ +
+
+

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). https://www.nice.org.uk/guidance/cg98

+

This tool is made available 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. Information provided in this tool is intended for reference by medical professionals. Nothing in this tool is intended to constitute medical advice.

+

Lee Yingtong Li, 2024. Source code available at https://yingtongli.me/git/bilirubin-calculator.

+
+
+ + + + diff --git a/main.css b/src/main.css similarity index 100% rename from main.css rename to src/main.css diff --git a/tailwind.config.js b/tailwind.config.js index 3f07528..c67e64b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,7 +19,7 @@ */ module.exports = { - content: ['./*.html'], + content: ['./src/*.html', './src/*.js'], theme: { extend: {}, fontFamily: {