nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
277 lines (234 loc) • 8.81 kB
JavaScript
;
var translate = require('../language')().translate;
var d3 = (global && global.d3) || require('d3');
var calibrations = {
name: 'calibrations'
, label: 'Calibrations'
, pluginType: 'report'
};
function init () {
return calibrations;
}
module.exports = init;
calibrations.html = function html (client) {
var translate = client.translate;
var ret =
'<h2>' + translate('Calibrations') + '</h2>' +
'<div style="width:50%;height:500px;float:left;overflow:scroll;overflow-x:hidden;" id="calibrations-list"></div>' +
'<div style="width:48%;float:right;" id="calibrations-chart"></div>';
return ret;
};
calibrations.report = function report_calibrations (datastorage, sorteddaystoshow) {
var Nightscout = window.Nightscout;
var report_plugins = Nightscout.report_plugins;
var padding = { top: 15, right: 15, bottom: 30, left: 70 };
var treatments = [];
sorteddaystoshow.forEach(function(day) {
treatments = treatments.concat(datastorage[day].treatments.filter(function(t) {
if (t.eventType === 'Sensor Start') {
return true;
}
if (t.eventType === 'Sensor Change') {
return true;
}
return false;
}));
});
var cals = [];
sorteddaystoshow.forEach(function(day) {
cals = cals.concat(datastorage[day].cal);
});
var sgvs = [];
sorteddaystoshow.forEach(function(day) {
sgvs = sgvs.concat(datastorage[day].sgv);
});
var mbgs = [];
sorteddaystoshow.forEach(function(day) {
mbgs = mbgs.concat(datastorage[day].mbg);
});
mbgs.forEach(function(mbg) { calcmbg(mbg); });
var events = treatments.concat(cals).concat(mbgs).sort(function(a, b) { return a.mills - b.mills; });
var colors = ['Aqua', 'Blue', 'Brown', 'Chartreuse', 'Coral', 'CornflowerBlue', 'DarkCyan', 'DarkMagenta', 'DarkOrange', 'Fuchsia', 'Green', 'Yellow'];
var colorindex = 0;
var html = '<table>';
var lastmbg = null;
for (var i = 0; i < events.length; i++) {
var e = events[i];
colorindex = (e.device !== undefined ? (colorindex + 1) % colors.length : colorindex);
var currentcolor = (!e.eventType ? colors[colorindex] : 'White');
html += '<tr>';
html += '<td>' + report_plugins.utils.localeDateTime(new Date(e.mills)) + '</td><td style="background-color:' + currentcolor + '">';
e.bgcolor = colors[colorindex];
if (e.eventType) {
html += '<b style="text-decoration: underline;padding-left:0em">' + translate(e.eventType) + '</b>:<br>';
} else if (typeof e.device !== 'undefined') {
html += '<input type="checkbox" index="' + i + '" class="calibrations-checkbox" id="calibrations-' + i + '"> ';
html += '<b style="padding-left:2em">MBG</b>: ' + e.y + ' Raw: ' + e.raw + '<br>';
lastmbg = e;
e.cals = [];
e.checked = false;
} else if (typeof e.scale !== 'undefined') {
html += '<b style="padding-left:4em">CAL</b>: ' + ' Scale: ' + e.scale.toFixed(2) + ' Intercept: ' + e.intercept.toFixed(0) + ' Slope: ' + e.slope.toFixed(2) + '<br>';
if (lastmbg) {
lastmbg.cals.push(e);
}
} else {
html += JSON.stringify(e);
}
html += '</td></tr>';
}
html += '</table>';
$('#calibrations-list').html(html);
// select last 3 mbgs
checkLastCheckboxes(3);
drawelements();
$('.calibrations-checkbox').change(checkboxevent);
function checkLastCheckboxes (maxcals) {
for (i = events.length - 1; i > 0; i--) {
if (typeof events[i].device !== 'undefined') {
events[i].checked = true;
$('#calibrations-' + i).prop('checked', true);
if (--maxcals < 1) {
break;
}
}
}
}
function checkboxevent (event) {
var index = $(this).attr('index');
events[index].checked = $(this).is(':checked');
drawelements();
event.preventDefault();
}
function drawelements () {
drawChart();
for (var i = 0; i < events.length; i++) {
e = events[i];
if (e.checked) {
drawmbg(e);
e.cals.forEach(drawcal);
}
}
}
var calibration_context, xScale2, yScale2;
function drawChart () {
var maxBG = 500;
$('#calibrations-chart').empty();
var charts = d3.select('#calibrations-chart').append('svg');
charts.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'WhiteSmoke');
calibration_context = charts.append('g');
// define the parts of the axis that aren't dependent on width or height
xScale2 = d3.scaleLinear()
.domain([0, maxBG]);
yScale2 = d3.scaleLinear()
.domain([0, 400000]);
var xAxis2 = d3.axisBottom(xScale2)
.ticks(10);
var yAxis2 = d3.axisLeft(yScale2);
// get current data range
var dataRange = [0, maxBG];
var width = 600;
var height = 500;
// get the entire container height and width subtracting the padding
var chartWidth = width - padding.left - padding.right;
var chartHeight = height - padding.top - padding.bottom;
//set the width and height of the SVG element
charts.attr('width', width)
.attr('height', height);
// ranges are based on the width and height available so reset
xScale2.range([0, chartWidth]);
yScale2.range([chartHeight, 0]);
// create the x axis container
calibration_context.append('g')
.attr('class', 'x axis');
// create the y axis container
calibration_context.append('g')
.attr('class', 'y axis');
calibration_context.select('.y')
.attr('transform', 'translate(' + ( /*chartWidth + */ padding.left) + ',' + padding.top + ')')
.style('stroke', 'black')
.style('shape-rendering', 'crispEdges')
.style('fill', 'none')
.call(yAxis2);
// if first run then just display axis with no transition
calibration_context.select('.x')
.attr('transform', 'translate(' + padding.left + ',' + (chartHeight + padding.top) + ')')
.style('stroke', 'black')
.style('shape-rendering', 'crispEdges')
.style('fill', 'none')
.call(xAxis2);
[50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000].forEach(function(li) {
calibration_context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale2(dataRange[0]) + padding.left)
.attr('y1', yScale2(li) + padding.top)
.attr('x2', xScale2(dataRange[1]) + padding.left)
.attr('y2', yScale2(li) + padding.top)
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
});
[50, 100, 150, 200, 250, 300, 350, 400, 450, 500].forEach(function(li) {
calibration_context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale2(li) + padding.left)
.attr('y1', padding.top)
.attr('x2', xScale2(li) + padding.left)
.attr('y2', chartHeight + padding.top)
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
});
}
function drawcal (cal) {
var color = cal.bgcolor;
var y1 = 50000;
var x1 = cal.scale * (y1 - cal.intercept) / cal.slope;
var y2 = 400000;
var x2 = cal.scale * (y2 - cal.intercept) / cal.slope;
calibration_context.append('line')
.attr('x1', xScale2(x1) + padding.left)
.attr('y1', yScale2(y1) + padding.top)
.attr('x2', xScale2(x2) + padding.left)
.attr('y2', yScale2(y2) + padding.top)
.style('stroke-width', 3)
.attr('stroke', color);
}
function calcmbg (mbg) {
var lastsgv = findlatest(new Date(mbg.mills), sgvs);
if (lastsgv) {
if (mbg.mills - lastsgv.mills > 5 * 60 * 1000) {
console.log('Last SGV too old for MBG. Time diff: ' + ((mbg.mills - lastsgv.mills) / 1000 / 60).toFixed(1) + ' min', mbg);
} else {
mbg.raw = lastsgv.filtered || lastsgv.unfiltered;
}
} else {
console.log('Last entry not found for MBG ', mbg);
}
}
function drawmbg (mbg) {
var color = mbg.bgcolor;
if (mbg.raw) {
calibration_context.append('circle')
.attr('cx', xScale2(mbg.y) + padding.left)
.attr('cy', yScale2(mbg.raw) + padding.top)
.attr('fill', color)
.style('opacity', 1)
.attr('stroke-width', 1)
.attr('stroke', 'black')
.attr('r', 5);
}
}
function findlatest (date, storage) {
var last = null;
var time = date.getTime();
for (var i = 0; i < storage.length; i++) {
if (storage[i].mills > time) {
return last;
}
last = storage[i];
}
return last;
}
};