Bilirubin chart: BiliTool/PediTools-style visual polish (no data changes)
Pure visual improvements to renderBiliChart. Interpolation, lookup
tables, and threshold math are all untouched.
AAP chart now has three-zone shading matching BiliTool's convention:
- Faint green tint below the phototherapy curve (safe zone)
- Amber band between phototherapy and exchange (treatment zone)
- Unshaded above the dashed crimson exchange line (danger)
Phototherapy line: solid orange 2.4 px; Exchange: dashed crimson.
renderBiliChart common improvements:
- Right-edge label on each threshold line ("Phototherapy", "Exchange",
or "95th (High-Risk)" for Bhutani) with white halo — identifiable
without a legend, like PediTools
- Legend removed (replaced by the inline labels)
- X-axis auto-ranges to fit the actual data with tick every 12 h
- Y-axis tick every 5 mg/dL for clean BiliTool-style gridlines
- 40 px right padding so labels don't clip
- Patient dot shrunk from r=8 to r=5 with a 1.8 px white ring,
redrawn on top of every label + its TSB value printed next to it
- Bhutani chart inherits all the same improvements without changing
its own zone/fill setup
This commit is contained in:
parent
7a7fd5b4eb
commit
e8a2283fae
1 changed files with 117 additions and 11 deletions
|
|
@ -1404,6 +1404,64 @@
|
||||||
return keys.map(function(h) { return { x: h, y: table[h] }; });
|
return keys.map(function(h) { return { x: h, y: table[h] }; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chart.js plugin — labels threshold lines at their right end (like
|
||||||
|
// BiliTool/PediTools do on their plots) and re-paints the patient dot
|
||||||
|
// on top so labels never cover it.
|
||||||
|
var biliLabelPlugin = {
|
||||||
|
id: 'biliThresholdLabels',
|
||||||
|
afterDatasetsDraw: function(chart) {
|
||||||
|
var ctx = chart.ctx;
|
||||||
|
var font = '700 11px -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif';
|
||||||
|
chart.data.datasets.forEach(function(ds, i) {
|
||||||
|
if (!ds.label || ds.label === 'Patient') return;
|
||||||
|
var meta = chart.getDatasetMeta(i);
|
||||||
|
if (!meta || meta.hidden || !meta.data || !meta.data.length) return;
|
||||||
|
var last = meta.data[meta.data.length - 1];
|
||||||
|
if (!last) return;
|
||||||
|
var color = typeof ds.borderColor === 'string' ? ds.borderColor : '#555';
|
||||||
|
var m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||||
|
if (m) color = 'rgb(' + m[1] + ',' + m[2] + ',' + m[3] + ')';
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = font;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.92)';
|
||||||
|
ctx.strokeText(ds.label, last.x + 5, last.y);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillText(ds.label, last.x + 5, last.y);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
// Patient dot on top
|
||||||
|
chart.data.datasets.forEach(function(ds, i) {
|
||||||
|
if (ds.label !== 'Patient') return;
|
||||||
|
var meta = chart.getDatasetMeta(i);
|
||||||
|
if (!meta || !meta.data || !meta.data.length) return;
|
||||||
|
var pt = meta.data[0];
|
||||||
|
if (!pt) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(pt.x, pt.y, 5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = '#2563eb';
|
||||||
|
ctx.fill();
|
||||||
|
ctx.lineWidth = 1.8;
|
||||||
|
ctx.strokeStyle = '#ffffff';
|
||||||
|
ctx.stroke();
|
||||||
|
// Value label next to the dot
|
||||||
|
ctx.font = '700 12px -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.95)';
|
||||||
|
var lbl = ds.data[0].y.toFixed(1);
|
||||||
|
ctx.strokeText(lbl, pt.x + 9, pt.y);
|
||||||
|
ctx.fillStyle = '#1e40af';
|
||||||
|
ctx.fillText(lbl, pt.x + 9, pt.y);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function renderBiliChart(patientHours, patientTSB, curveDatasets) {
|
function renderBiliChart(patientHours, patientTSB, curveDatasets) {
|
||||||
if (typeof Chart === 'undefined') return;
|
if (typeof Chart === 'undefined') return;
|
||||||
var container = document.getElementById('bili-chart-container');
|
var container = document.getElementById('bili-chart-container');
|
||||||
|
|
@ -1416,33 +1474,61 @@
|
||||||
return Object.assign({ pointRadius: 0, pointHoverRadius: 0, tension: 0.3, order: 10 }, ds);
|
return Object.assign({ pointRadius: 0, pointHoverRadius: 0, tension: 0.3, order: 10 }, ds);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Patient point
|
// Patient point (native dataset — used for tooltip + legend. The plugin
|
||||||
|
// re-paints it on top at the end so threshold labels never cover it.)
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: 'Patient',
|
label: 'Patient',
|
||||||
data: [{ x: patientHours, y: patientTSB }],
|
data: [{ x: patientHours, y: patientTSB }],
|
||||||
backgroundColor: '#2563eb',
|
backgroundColor: '#2563eb',
|
||||||
borderColor: '#1d4ed8',
|
borderColor: '#ffffff',
|
||||||
pointRadius: 8,
|
borderWidth: 1.8,
|
||||||
pointHoverRadius: 10,
|
pointRadius: 5,
|
||||||
|
pointHoverRadius: 7,
|
||||||
showLine: false,
|
showLine: false,
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
order: 0
|
order: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Derive nice x-axis range from actual data (clamped to a minimum
|
||||||
|
// visible window so single-point patients still show in context).
|
||||||
|
var xMax = patientHours + 6;
|
||||||
|
var xMin = 12;
|
||||||
|
curveDatasets.forEach(function(ds) {
|
||||||
|
(ds.data || []).forEach(function(p) {
|
||||||
|
if (typeof p.x === 'number') xMax = Math.max(xMax, p.x);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
xMax = Math.min(Math.max(xMax, 72), 180);
|
||||||
|
|
||||||
_chartInstances['bili-chart-canvas'] = new Chart(document.getElementById('bili-chart-canvas'), {
|
_chartInstances['bili-chart-canvas'] = new Chart(document.getElementById('bili-chart-canvas'), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { datasets: datasets },
|
data: { datasets: datasets },
|
||||||
|
plugins: [biliLabelPlugin],
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: true,
|
||||||
aspectRatio: 1.6,
|
aspectRatio: 1.6,
|
||||||
animation: { duration: 300 },
|
animation: { duration: 300 },
|
||||||
|
// Extra right padding so the threshold labels drawn past the
|
||||||
|
// last data point stay inside the canvas.
|
||||||
|
layout: { padding: { right: 40 } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { type: 'linear', title: { display: true, text: 'Age (hours)', font: { size: 12, weight: 'bold' } }, min: 12, max: 150, grid: { color: 'rgba(0,0,0,0.05)' } },
|
x: {
|
||||||
y: { title: { display: true, text: 'TSB (mg/dL)', font: { size: 12, weight: 'bold' } }, min: 0, grid: { color: 'rgba(0,0,0,0.05)' } }
|
type: 'linear',
|
||||||
|
title: { display: true, text: 'Age (hours)', font: { size: 12, weight: 'bold' } },
|
||||||
|
min: xMin, max: xMax,
|
||||||
|
ticks: { stepSize: 12, font: { size: 11 } },
|
||||||
|
grid: { color: 'rgba(0,0,0,0.08)' }
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: { display: true, text: 'TSB (mg/dL)', font: { size: 12, weight: 'bold' } },
|
||||||
|
min: 0,
|
||||||
|
ticks: { stepSize: 5, font: { size: 11 } },
|
||||||
|
grid: { color: 'rgba(0,0,0,0.08)' }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true, position: 'bottom', labels: { font: { size: 11 }, usePointStyle: true, pointStyle: 'line' } },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function(ctx) {
|
label: function(ctx) {
|
||||||
|
|
@ -1553,12 +1639,32 @@
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">GA ' + ga + ' weeks, ' + (risk === 'medium' ? 'with' : 'without') + ' neurotoxicity risk factors. AAP 2022 CPG (exact values from published nomograms). Always use clinical judgment.</p>';
|
'<p style="font-size:11px;color:var(--g400);margin:12px 0 0;">GA ' + ga + ' weeks, ' + (risk === 'medium' ? 'with' : 'without') + ' neurotoxicity risk factors. AAP 2022 CPG (exact values from published nomograms). Always use clinical judgment.</p>';
|
||||||
|
|
||||||
// Chart: phototherapy + exchange threshold lines vs patient
|
// Chart: clinical BiliTool/PediTools-style layout
|
||||||
|
// - Green tint below the phototherapy line (safe zone)
|
||||||
|
// - Amber tint between phototherapy and exchange (treatment zone)
|
||||||
|
// - Dashed crimson exchange line above (danger threshold)
|
||||||
var chartDatasets = [
|
var chartDatasets = [
|
||||||
{ label: 'Phototherapy Threshold', data: generateThresholdCurve(table), borderColor: '#ef4444', borderWidth: 2, borderDash: [], fill: { target: 'origin', above: 'rgba(239,68,68,0.08)' } }
|
{
|
||||||
|
label: 'Phototherapy',
|
||||||
|
data: generateThresholdCurve(table),
|
||||||
|
borderColor: '#f97316', // orange line
|
||||||
|
borderWidth: 2.4,
|
||||||
|
borderDash: [],
|
||||||
|
fill: 'origin',
|
||||||
|
backgroundColor: 'rgba(22,163,74,0.07)' // faint green below = "safe"
|
||||||
|
}
|
||||||
];
|
];
|
||||||
if (exchangeTable) {
|
if (exchangeTable) {
|
||||||
chartDatasets.push({ label: 'Exchange Transfusion', data: generateThresholdCurve(exchangeTable), borderColor: '#7f1d1d', borderWidth: 2, borderDash: [6,3], fill: false });
|
chartDatasets.push({
|
||||||
|
label: 'Exchange',
|
||||||
|
data: generateThresholdCurve(exchangeTable),
|
||||||
|
borderColor: '#b91c1c', // crimson line
|
||||||
|
borderWidth: 2.4,
|
||||||
|
borderDash: [6, 3],
|
||||||
|
// Fill between this line and the phototherapy line (dataset 0)
|
||||||
|
fill: { target: 0 },
|
||||||
|
backgroundColor: 'rgba(252,211,77,0.22)' // amber band = "treat"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
renderBiliChart(hours, tsb, chartDatasets);
|
renderBiliChart(hours, tsb, chartDatasets);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue