From f1041dd1713e9dab139aa46d9beae7ce76d69b94 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 25 Jul 2025 19:47:29 +1000 Subject: [PATCH] Implement RWH charts --- src/bilirubin_app.js | 37 ++++++++--- src/bilirubin_lib.js | 138 ++++++++++++++++++++++++++++++++++++++--- tests/describe.test.js | 22 +++++-- 3 files changed, 174 insertions(+), 23 deletions(-) diff --git a/src/bilirubin_app.js b/src/bilirubin_app.js index 3e83d03..41ad280 100644 --- a/src/bilirubin_app.js +++ b/src/bilirubin_app.js @@ -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(); diff --git a/src/bilirubin_lib.js b/src/bilirubin_lib.js index e4b3712..64b65e7 100644 --- a/src/bilirubin_lib.js +++ b/src/bilirubin_lib.js @@ -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 exchange transfusion 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'); diff --git a/tests/describe.test.js b/tests/describe.test.js index 66a1da2..71d844b 100644 --- a/tests/describe.test.js +++ b/tests/describe.test.js @@ -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 180 below 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 40 below 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 20 above 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 exchange transfusion 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);