<!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>
				<div id="result" class="d-none">
					<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) {
					throw new Exception('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 Exception('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;
				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: '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
			
			let dateNow = new Date();
			dateNow.setSeconds(0);
			dateNow.setMilliseconds(0);
			
			if (document.getElementById('time_birth').value === '') {
				document.getElementById('time_birth').valueAsDate = dateNow;
			}
			if (document.getElementById('time_measurement').value === '') {
				document.getElementById('time_measurement').valueAsDate = dateNow;
			}
			
			// --------------------------
			// Plot bilirubin measurement
			
			function prettyPrintBilirubin(b) {
				if (b < 10) {
					return b.toFixed(1);
				} else {
					return Math.round(b).toFixed(0);
				}
			}
			
			function updateBilirubin() {
				if (document.getElementById('time_birth').valueAsDate === null) {
					return;
				}
				if (document.getElementById('time_measurement').valueAsDate === null) {
					return;
				}
				if (document.getElementById('bilirubin').value === '') {
					return;
				}
				
				// Chronological age in days
				let d = (document.getElementById('time_measurement').valueAsDate.getTime() - document.getElementById('time_birth').valueAsDate.getTime()) / 1000 / 60 / 60 / 24;
				
				let bilirubin = document.getElementById('bilirubin').valueAsNumber;
				let gestation = document.getElementById('gestation').valueAsNumber;
				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();
				
				let resultDiv = document.getElementById('result');
				let resultP = resultDiv.querySelector('p');
				
				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>