Implement RWH charts

This commit is contained in:
RunasSudo 2025-07-25 19:47:29 +10:00
parent 4820669558
commit f1041dd171
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 174 additions and 23 deletions

View File

@ -63,9 +63,14 @@ function plotGraphData() {
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') {
// NYI
chart.data.datasets[0].data = [];
chart.data.datasets[1].data = [];
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');
}
@ -191,23 +196,35 @@ function updateBilirubin() {
return;
}
let d, result_text, text_colour, background_colour, accent_colour;
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') {
[d, result_text, text_colour, background_colour, accent_colour] = describeBilirubin(gestation, new Date(document.getElementById('time_birth').value), new Date(document.getElementById('time_measurement').value), bilirubin);
phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
exchange_thresh_value = nice_exchange_thresh(d, gestation);
} else if (guideline === 'rwh') {
// NYI
resultDiv.className = 'hidden';
return;
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');
}
chart.data.datasets[2].data = [{x: d, y: bilirubin}];
chart.update();
// 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();

View File

@ -76,16 +76,132 @@ function nice_exchange_thresh(d, gestation) {
return thresh_at_3days;
}
// ----------------------------
// Threshold calculations (RWH)
var RWH_CURVE_DATA = {
'geq38': {
'phototherapy': [[0, 110], [12, 160], [24, 205], [48, 265], [60, 290], [72, 310], [96, 340], [120, 360]],
'exchange': [[0, 270], [12, 295], [24, 325], [48, 375], [60, 390], [72, 410], [96, 430], [108, 430], [120, 435]]
},
'35-37': {
'phototherapy': [[0, 85], [12, 130], [24, 170], [36, 200], [48, 225], [60, 240], [72, 260], [96, 290]],
'exchange': [[0, 235], [12, 260], [36, 300], [48, 325], [60, 340], [72, 360], [84, 375], [96, 385]],
},
'33-34': {
'phototherapy_datpos': [[0, 80], [12, 110], [24, 145], [36, 170], [48, 190], [72, 220]],
'phototherapy_datneg': [[0, 100], [12, 130], [24, 165], [36, 190], [48, 210], [72, 240]],
'exchange': [[0, 210], [12, 240], [24, 260], [36, 295], [48, 315], [60, 330], [72, 340]],
},
'31-32': {
'phototherapy_datpos': [[0, 70], [12, 100], [24, 135], [48, 175], [60, 190], [72, 200]],
'phototherapy_datneg': [[0, 90], [12, 120], [24, 155], [48, 195], [60, 210], [72, 220]],
'exchange': [[0, 190], [12, 225], [24, 255], [48, 295], [60, 310], [72, 320]],
},
'27-30': {
'phototherapy_datpos': [[0, 60], [24, 120], [36, 140], [48, 155], [60, 165], [72, 170]],
'phototherapy_datneg': [[0, 80], [24, 140], [36, 160], [48, 175], [60, 185], [72, 190]],
'exchange': [[0, 170], [24, 230], [48, 270], [72, 290]],
},
'lt27': {
'phototherapy_datpos': [[0, 60], [12, 70], [24, 90], [48, 120], [72, 130]],
'phototherapy_datneg': [[0, 80], [12, 90], [24, 110], [48, 140], [72, 150]],
'exchange': [[0, 150], [12, 180], [24, 205], [48, 235], [60, 245], [72, 250]],
},
};
function rwh_interpolate_curve(hours, curve) {
if (hours < curve[0][0]) {
throw new Error('Attempt to interpolate before start of curve');
}
let leftHours = curve[0][0];
let leftBilirubin = curve[0][1];
for (let i = 1; i < curve.length; i++) {
let rightHours = curve[i][0];
let rightBilirubin = curve[i][1];
if (hours <= rightHours) {
return leftBilirubin + (hours - leftHours) / (rightHours - leftHours) * (rightBilirubin - leftBilirubin);
}
leftHours = rightHours;
leftBilirubin = rightBilirubin;
}
// To the right of the final point = flat part of curve
return leftBilirubin;
}
function rwh_phototherapy_geq35(d, chart) {
return rwh_interpolate_curve(d * 24, RWH_CURVE_DATA[chart]['phototherapy']);
}
function rwh_phototherapy_lt35(d, chart, dat) {
if (dat) {
return rwh_interpolate_curve(d * 24, RWH_CURVE_DATA[chart]['phototherapy_datpos']);
} else {
return rwh_interpolate_curve(d * 24, RWH_CURVE_DATA[chart]['phototherapy_datneg']);
}
}
function rwh_exchange(d, chart) {
return rwh_interpolate_curve(d * 24, RWH_CURVE_DATA[chart]['exchange']);
}
function rwh_phototherapy_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising) {
// Determine the correct chart
if (gestation >= 38) {
if (dat || bw_lt2500 || fhx_rbc || bruising) {
return rwh_phototherapy_geq35(d, '35-37');
} else {
return rwh_phototherapy_geq35(d, 'geq38');
}
} else if (gestation >= 35) {
return rwh_phototherapy_geq35(d, '35-37');
} else {
// Gestation < 35 weeks
if (bw_lt1000 || gestation < 27) {
return rwh_phototherapy_lt35(d, 'lt27', dat);
} else if (gestation < 31) {
return rwh_phototherapy_lt35(d, '27-30', dat);
} else if (gestation < 33) {
return rwh_phototherapy_lt35(d, '31-32', dat);
} else {
return rwh_phototherapy_lt35(d, '33-34', dat);
}
}
}
function rwh_exchange_thresh(d, gestation, dat, bw_lt2500, bw_lt1000, fhx_rbc, bruising) {
// Determine the correct chart
if (gestation >= 38) {
if (dat || bw_lt2500 || fhx_rbc || bruising) {
return rwh_exchange(d, '35-37');
} else {
return rwh_exchange(d, 'geq38');
}
} else if (gestation >= 35) {
return rwh_exchange(d, '35-37');
} else {
// Gestation < 35 weeks
if (bw_lt1000 || gestation < 27) {
return rwh_exchange(d, 'lt27');
} else if (gestation < 31) {
return rwh_exchange(d, '27-30');
} else if (gestation < 33) {
return rwh_exchange(d, '31-32');
} else {
return rwh_exchange(d, '33-34');
}
}
}
// ----------------------------------
// Bilirubin vs threshold calculation
function describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin) {
// Chronological age in days
let d = (dateMeasurement.getTime() - dateBirth.getTime()) / 1000 / 60 / 60 / 24;
let phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
let exchange_thresh_value = nice_exchange_thresh(d, gestation);
function describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value) {
// Restate colours separately so Tailwind can detect them
let result_text, text_colour, background_colour, accent_colour;
@ -115,12 +231,18 @@ function describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin) {
result_text = 'Bilirubin of ' + bilirubin + ' at ' + prettyPrintDays(d) + ' is ' + prettyPrintBilirubin(bilirubin - exchange_thresh_value) + ' above the <span class="font-bold">exchange transfusion</span> threshold.';
}
return [d, result_text, text_colour, background_colour, accent_colour];
return [result_text, text_colour, background_colour, accent_colour];
}
// --------------
// Utility functions
function chronoAgeInDays(dateBirth, dateMeasurement) {
// Chronological age in days
let d = (dateMeasurement.getTime() - dateBirth.getTime()) / 1000 / 60 / 60 / 24;
return d;
}
function prettyPrintHours(d) {
if (d >= 23.5/24) {
throw new Error('>24 hours passed to prettyPrintHours');

View File

@ -1,6 +1,6 @@
/*
Neonatal jaundice treatment threshold calculator
Copyright (C) 2024 Lee Yingtong Li
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
@ -33,7 +33,10 @@ suite('Describe bilirubin result', () => {
const expectedText = 'Bilirubin of 20 at 1 day, 0 hours is <span class="font-bold">180 below</span> the phototherapy threshold.';
const expectedColour = 'sky';
let [d, result_text, text_colour, background_colour, accent_colour] = describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin);
let d = chronoAgeInDays(dateBirth, dateMeasurement);
let phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
let exchange_thresh_value = nice_exchange_thresh(d, gestation);
let [result_text, text_colour, background_colour, accent_colour] = describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value);
assert.equal(d, 1);
assert.equal(result_text, expectedText);
@ -47,7 +50,10 @@ suite('Describe bilirubin result', () => {
const expectedText = 'Bilirubin of 160 at 1 day, 0 hours is <span class="font-bold">40 below</span> the phototherapy threshold.';
const expectedColour = 'yellow';
let [d, result_text, text_colour, background_colour, accent_colour] = describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin);
let d = chronoAgeInDays(dateBirth, dateMeasurement);
let phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
let exchange_thresh_value = nice_exchange_thresh(d, gestation);
let [result_text, text_colour, background_colour, accent_colour] = describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value);
assert.equal(d, 1);
assert.equal(result_text, expectedText);
@ -61,7 +67,10 @@ suite('Describe bilirubin result', () => {
const expectedText = 'Bilirubin of 220 at 1 day, 0 hours is <span class="font-bold">20 above</span> the phototherapy threshold, 80 below the exchange transfusion threshold.';
const expectedColour = 'orange';
let [d, result_text, text_colour, background_colour, accent_colour] = describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin);
let d = chronoAgeInDays(dateBirth, dateMeasurement);
let phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
let exchange_thresh_value = nice_exchange_thresh(d, gestation);
let [result_text, text_colour, background_colour, accent_colour] = describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value);
assert.equal(d, 1);
assert.equal(result_text, expectedText);
@ -75,7 +84,10 @@ suite('Describe bilirubin result', () => {
const expectedText = 'Bilirubin of 350 at 1 day, 0 hours is 50 above the <span class="font-bold">exchange transfusion</span> threshold.';
const expectedColour = 'red';
let [d, result_text, text_colour, background_colour, accent_colour] = describeBilirubin(gestation, dateBirth, dateMeasurement, bilirubin);
let d = chronoAgeInDays(dateBirth, dateMeasurement);
let phototherapy_thresh_value = nice_phototherapy_thresh(d, gestation);
let exchange_thresh_value = nice_exchange_thresh(d, gestation);
let [result_text, text_colour, background_colour, accent_colour] = describeBilirubin(d, bilirubin, phototherapy_thresh_value, exchange_thresh_value);
assert.equal(d, 1);
assert.equal(result_text, expectedText);