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.
1,113 lines (1,000 loc) • 50 kB
JavaScript
;
var _ = require('lodash');
var moment = window.moment;
var times = require('../times');
var d3 = (global && global.d3) || require('d3');
var daytoday = {
name: 'daytoday'
, label: 'Day to day'
, pluginType: 'report'
};
function init () {
return daytoday;
}
module.exports = init;
daytoday.html = function html (client) {
const reportStorage = require('../report/reportstorage');
var translate = client.translate;
var ret =
'<h2>' + translate('Day to day') + '</h2>' +
'<b>' + translate('To see this report, press SHOW while in this view') + '</b><br>' +
translate('Display') + ': ' +
`<label><input type="checkbox" id="rp_optionsinsulin" ${reportStorage.getValue('insulin') ? "checked" : ""}><span style="color:blue;opacity:0.5"> ${translate('Insulin')} </span></label>` +
`<label><input type="checkbox" id="rp_optionscarbs" ${reportStorage.getValue('carbs') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('Carbs')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsbasal" ${reportStorage.getValue('basal') ? "checked" : ""}><span style="color:#0099ff;opacity:0.5">${translate('Basal rate')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsnotes" ${reportStorage.getValue('notes') ? "checked" : ""}>${translate('Notes')}</label>` +
`<label><input type="checkbox" id="rp_optionsfood" ${reportStorage.getValue('food') ? "checked" : ""}>${translate('Food')}</label>` +
`<label><input type="checkbox" id="rp_optionsraw" ${reportStorage.getValue('raw') ? "checked" : ""}><span style="color:gray;opacity:1">${translate('Raw')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsiob" ${reportStorage.getValue('iob') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('IOB')}</span></label>` +
`<label><input type="checkbox" id="rp_optionscob" ${reportStorage.getValue('cob') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('COB')}</span></label>` +
`<label><input type="checkbox" id="rp_optionspredicted" ${reportStorage.getValue('predicted') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('Predictions')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsopenaps" ${reportStorage.getValue('openAps') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('OpenAPS')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsdistribution" ${reportStorage.getValue('insulindistribution') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('Insulin distribution')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsbgcheck" ${reportStorage.getValue('bgcheck') ? "checked" : ""}><span style="color:#ff0000;opacity:0.5">${translate('BG Check')}</span></label>` +
`<label><input type="checkbox" id="rp_optionsothertreatments" ${reportStorage.getValue('othertreatments') ? "checked" : ""}>${translate('View all treatments')}</span></label>` +
' ' + translate('Size') +
' <select id="rp_size">' +
' <option x="800" y="250">800x250px</option>' +
' <option x="1000" y="300" selected>1000x300px</option>' +
' <option x="1200" y="400">1200x400px</option>' +
' <option x="1550" y="600">1550x600px</option>' +
' <option x="2400" y="800">2400x800px</option>' +
'</select>' +
'<br>' +
translate('Scale') + ': ' +
'<label><input type="radio" name="rp_scale" id="rp_linear" checked>' +
translate('Linear') + '</label>' +
'<label><input type="radio" name="rp_scale" id="rp_log">' +
translate('Logarithmic') + '</label>' +
`<div id="rp_predictedSettings" ${reportStorage.getValue('predicted') ? '' : 'style="display:none"'}>` +
translate('Truncate predictions: ') +
`<input type="checkbox" id="rp_optionsPredictedTruncate" ${reportStorage.getValue('predictedTruncate') ? "checked" : ""}>` +
'<br>' +
translate('Predictions offset') + ': ' +
'<b><label id="rp_predictedOffset"></label> minutes</b>' +
' ' +
'<input type="button" onclick="Nightscout.predictions.moreBackward();" value="' + translate('-30 min') + '">' +
'<input type="button" onclick="Nightscout.predictions.backward();" value="' + translate('-5 min') + '">' +
'<input type="button" onclick="Nightscout.predictions.reset();" value="' + translate('Zero') + '">' +
'<input type="button" onclick="Nightscout.predictions.forward();" value="' + translate('+5 min') + '">' +
'<input type="button" onclick="Nightscout.predictions.moreForward();" value="' + translate('+30 min') + '">' +
'</div>' +
'<br>' +
'<div id="daytodaycharts">' +
'</div>';
return ret;
};
daytoday.prepareHtml = function daytodayPrepareHtml (sorteddaystoshow) {
$('#daytodaycharts').html('');
sorteddaystoshow.forEach(function eachDay (d) {
$('#daytodaycharts').append($('<table><tr><td><div id="daytodaychart-' + d + '"></div></td><td><div id="daytodaystatchart-' + d + '"></td></tr></table>'));
});
};
daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, options) {
var Nightscout = window.Nightscout;
var client = Nightscout.client;
var translate = client.translate;
var profile = client.sbx.data.profile;
var report_plugins = Nightscout.report_plugins;
var scaledTreatmentBG = report_plugins.utils.scaledTreatmentBG;
var padding = { top: 15, right: 22, bottom: 30, left: 35 };
var tddSum = 0;
var basalSum = 0;
var baseBasalSum = 0;
var bolusSum = 0;
var carbsSum = 0;
var proteinSum = 0;
var fatSum = 0;
daytoday.prepareHtml(sorteddaystoshow);
sorteddaystoshow.forEach(function eachDay (day) {
drawChart(day, datastorage[day], options);
});
var tddAverage = tddSum / datastorage.alldays;
var basalAveragePercent = Math.round( (basalSum / datastorage.alldays) / tddAverage * 100);
var baseBasalAveragePercent = Math.round( (baseBasalSum / datastorage.alldays) / tddAverage * 100);
var bolusAveragePercent = Math.round( (bolusSum / datastorage.alldays) / tddAverage * 100);
var carbsAverage = carbsSum / datastorage.alldays;
var proteinAverage = proteinSum / datastorage.alldays;
var fatAverage = fatSum / datastorage.alldays;
if (options.insulindistribution) {
var html = '<br><br><b>' + translate('TDD average') + ':</b> ' + tddAverage.toFixed(1) + 'U ';
html += '<b>' + translate('Bolus average') + ':</b> ' + bolusAveragePercent + '% ';
html += '<b>' + translate('Basal average') + ':</b> ' + basalAveragePercent + '% ';
html += '<b>(' + translate('Base basal average:') + '</b> ' + baseBasalAveragePercent + '%<b>)</b> ';
html += '<b>' + translate('Carbs average') + ':</b> ' + carbsAverage.toFixed(0) + 'g';
html += '<b>' + translate('Protein average') + ':</b> ' + proteinAverage.toFixed(0) + 'g';
html += '<b>' + translate('Fat average') + ':</b> ' + fatAverage.toFixed(0) + 'g';
$('#daytodaycharts').append(html);
}
function timeTicks(n,i) {
var t12 = [
'12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '',
'12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am'
];
if (Nightscout.client.settings.timeFormat === 24) {
return ('00' + i).slice(-2);
} else {
return t12[i];
}
}
function drawChart (day, data, options) {
var tickValues
, charts
, context
, xScale2, yScale2
, yInsulinScale, yCarbsScale, yScaleBasals
, xAxis2, yAxis2
, dateFn = function(d) { return new Date(d.date); }
, foodtexts = 0;
tickValues = client.ticks(client, {
scaleY: options.scale === report_plugins.consts.SCALE_LOG ? 'log' : 'linear'
, targetTop: options.targetHigh
, targetBottom: options.targetLow
});
// add defs for combo boluses
var dashWidth = 5;
d3.select('body').append('svg')
.append('defs')
.append('pattern')
.attr('id', 'hash')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 6)
.attr('height', 6)
.attr('x', 0)
.attr('y', 0)
.append('g')
.style('fill', 'none')
.style('stroke', '#0099ff')
.style('stroke-width', 2)
.append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth)
.append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth);
// create svg and g to contain the chart contents
charts = d3.select('#daytodaychart-' + day).html(
'<b>' +
report_plugins.utils.localeDate(day) +
'</b><br>'
).append('svg');
charts.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'WhiteSmoke');
context = charts.append('g');
// define the parts of the axis that aren't dependent on width or height
xScale2 = d3.scaleTime()
.domain(d3.extent(data.sgv, dateFn));
if (options.scale === report_plugins.consts.SCALE_LOG) {
yScale2 = d3.scaleLog()
.domain([client.utils.scaleMgdl(options.basal ? 30 : 36), client.utils.scaleMgdl(420)]);
} else {
yScale2 = d3.scaleLinear()
.domain([client.utils.scaleMgdl(options.basal ? -40 : 36), client.utils.scaleMgdl(420)]);
}
// allow insulin to be negative (when plotting negative IOB)
yInsulinScale = d3.scaleLinear()
.domain([-2 * options.maxInsulinValue, 2 * options.maxInsulinValue]);
yCarbsScale = d3.scaleLinear()
.domain([0, options.maxCarbsValue * 1.25]);
yScaleBasals = d3.scaleLinear();
xAxis2 = d3.axisBottom(xScale2)
.tickFormat(timeTicks)
.ticks(24);
yAxis2 = d3.axisLeft(yScale2)
.tickFormat(d3.format('d'))
.tickValues(tickValues);
// get current data range
var dataRange = d3.extent(data.sgv, dateFn);
// get the entire container height and width subtracting the padding
var chartWidth = options.width - padding.left - padding.right;
var chartHeight = options.height - padding.top - padding.bottom;
//set the width and height of the SVG element
charts.attr('width', options.width)
.attr('height', options.height);
// ranges are based on the width and height available so reset
xScale2.range([0, chartWidth]);
yScale2.range([chartHeight, 0]);
yInsulinScale.range([chartHeight * 2, 0]);
yCarbsScale.range([chartHeight, 0]);
yScaleBasals.range([yScale2(client.utils.scaleMgdl(72)), chartHeight]);
// add target BG rect
context.append('rect')
.attr('x', xScale2(dataRange[0]) + padding.left)
.attr('y', yScale2(options.targetHigh) + padding.top)
.attr('width', xScale2(dataRange[1] - xScale2(dataRange[0])))
.attr('height', yScale2(options.targetLow) - yScale2(options.targetHigh))
.style('fill', '#D6FFD6')
.attr('stroke', 'grey');
// create the x axis container
context.append('g')
.attr('class', 'x axis');
// create the y axis container
context.append('g')
.attr('class', 'y axis');
context.select('.y')
.attr('transform', 'translate(' + (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
context.select('.x')
.attr('transform', 'translate(' + padding.left + ',' + (chartHeight + padding.top) + ')')
.style('stroke', 'black')
.style('shape-rendering', 'crispEdges')
.style('fill', 'none')
.call(xAxis2);
_.each(tickValues, function(n, li) {
context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale2(dataRange[0]) + padding.left)
.attr('y1', yScale2(tickValues[li]) + padding.top)
.attr('x2', xScale2(dataRange[1]) + padding.left)
.attr('y2', yScale2(tickValues[li]) + padding.top)
.style('stroke-dasharray', ('1, 5'))
.attr('stroke', 'grey');
});
function prepareContextCircles (sel) {
var badData = [];
sel.attr('cx', function(d) {
return xScale2(d.date) + padding.left;
})
.attr('cy', function(d) {
if (isNaN(d.sgv)) {
badData.push(d);
return yScale2(client.utils.scaleMgdl(450) + padding.top);
} else {
return yScale2(d.sgv) + padding.top;
}
})
.attr('fill', function(d) {
if (d.color === 'gray' && !options.raw) {
return 'transparent';
}
return d.color;
})
.style('opacity', function() { return 0.5 })
.attr('stroke-width', function(d) { if (d.type === 'mbg') { return 2; } else if (options.openAps && d.openaps) { return 1; } else { return 0; } })
.attr('stroke', function() { return 'black'; })
.attr('r', function(d) {
if (d.type === 'mbg') {
return 4;
} else {
return 2 + (options.width - 800) / 400;
}
})
.on('mouseover', function(d) {
if (options.openAps && d.openaps) {
client.tooltip.style('opacity', .9);
var text = '<b>BG:</b> ' + d.openaps.suggested.bg +
', ' + d.openaps.suggested.reason +
(d.openaps.suggested.mealAssist ? ' <b>Meal Assist:</b> ' + d.openaps.suggested.mealAssist : '');
client.tooltip.html(text)
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY + 15) + 'px');
}
})
.on('mouseout', hideTooltip);
if (badData.length > 0) {
console.warn('Bad Data: isNaN(sgv)', badData);
}
return sel;
}
// PREDICTIONS START
//
function preparePredictedData () {
var treatmentsTimestamps = []; // Only timestamps for (carbs and bolus insulin) treatments will be captured in this array
treatmentsTimestamps.push(dataRange[0]); // Create a fake timestamp at midnight so we can show predictions during night
for (var i in data.treatments) {
var treatment = data.treatments[i];
if (undefined != treatment.carbs && null != treatment.carbs && treatment.carbs > 0) {
if (treatment.timestamp)
treatmentsTimestamps.push(treatment.timestamp);
else if (treatment.created_at)
treatmentsTimestamps.push(treatment.created_at);
}
if (undefined != treatment.insulin && null != treatment.insulin && treatment.insulin > 0) {
if (treatment.timestamp)
treatmentsTimestamps.push(treatment.timestamp);
else if (treatment.created_at)
treatmentsTimestamps.push(treatment.created_at);
}
}
var predictions = [];
if (data && data.devicestatus) {
for (i = data.devicestatus.length - 1; i >= 0; i--) {
if (data.devicestatus[i].loop && data.devicestatus[i].loop.predicted) {
predictions.push(data.devicestatus[i].loop.predicted);
} else if (data.devicestatus[i].openaps && data.devicestatus[i].openaps.suggested && data.devicestatus[i].openaps.suggested.predBGs) {
var entry = {};
entry.startDate = data.devicestatus[i].openaps.suggested.timestamp;
// For OpenAPS/AndroidAPS we fall back from COB if present, to UAM, then IOB
if (data.devicestatus[i].openaps.suggested.predBGs.COB) {
entry.values = data.devicestatus[i].openaps.suggested.predBGs.COB;
} else if (data.devicestatus[i].openaps.suggested.predBGs.UAM) {
entry.values = data.devicestatus[i].openaps.suggested.predBGs.UAM;
} else entry.values = data.devicestatus[i].openaps.suggested.predBGs.IOB;
predictions.push(entry);
}
}
}
var p = [];
if (predictions.length > 0 && treatmentsTimestamps.length > 0) {
// Iterate over all treatments, find the predictions for each and add them to the predicted array p
for (var treatmentsIndex = 0; treatmentsIndex < treatmentsTimestamps.length; treatmentsIndex++) {
var timestamp = treatmentsTimestamps[treatmentsIndex];
// TODO refactor code so this is set here - now set as global in file loaded by the browser
var predictedIndex = findPredicted(predictions, timestamp, Nightscout.predictions.offset); // Find predictions offset before or after timestamp
if (predictedIndex != null) {
entry = predictions[predictedIndex]; // Start entry
var d = moment(entry.startDate);
var end = moment().endOf('day');
if (options.predictedTruncate) {
if (Nightscout.predictions.offset >= 0) {
// If we are looking forward we want to stop at the next treatment
if (treatmentsIndex < treatmentsTimestamps.length - 1) {
end = moment(treatmentsTimestamps[treatmentsIndex + 1]);
}
} else {
// If we are looking back, then we want to stop at "this" treatment
end = moment(treatmentsTimestamps[treatmentsIndex]);
}
}
for (var entryIndex in entry.values) {
if (!d.isAfter(end)) {
var value = {};
value.sgv = client.utils.scaleMgdl(entry.values[entryIndex]);
value.date = d.toDate();
value.color = 'purple';
p.push(value);
d.add(5, 'minutes');
}
}
}
}
}
return p;
}
/* Find the earliest new predicted instance that has a timestamp equal to or larger than timestamp */
/* (so if we have bolused or eaten we want to find the prediction that Loop has estimated just after that) */
/* Returns the index into the predictions array that is the predicted we are looking for */
function findPredicted (predictions, timestamp, offset) {
var ts = moment(timestamp).add(offset, 'minutes');
var predicted = null;
if (offset && offset < 0) { // If offset is negative, start searching from first prediction going forward
for (var i = 0; i < predictions.length; i++) {
if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) <= ts) {
predicted = i;
}
}
} else { // If offset is positive or zero, start searching from last prediction going backward
for (i = predictions.length - 1; i > 0; i--) {
if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) >= ts) {
predicted = i;
}
}
}
return predicted;
}
//
// PREDICTIONS ENDS
// bind up the context chart data to an array of circles
var contextData = (options.predicted ? data.sgv.concat(preparePredictedData()) : data.sgv);
var contextCircles = context.selectAll('circle').data(contextData);
// if new circle then just display
prepareContextCircles(contextCircles.enter().append('circle'));
contextCircles.exit()
.remove();
var to = moment(day).add(1, 'days');
var from = moment(day);
var iobpolyline = ''
, cobpolyline = '';
// basals data
var linedata = [];
var notemplinedata = [];
var basalareadata = [];
var tempbasalareadata = [];
var comboareadata = [];
var lastbasal = 0;
var lastDt = from;
var lastIOB;
var basals = context.append('g');
data.netBasalPositive = [];
data.netBasalNegative = [];
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23].forEach(function(hour) {
data.netBasalPositive[hour] = 0;
data.netBasalNegative[hour] = 0;
});
profile.loadData(datastorage.profiles);
profile.updateTreatments(datastorage.profileSwitchTreatments, datastorage.tempbasalTreatments, datastorage.combobolusTreatments);
var bolusInsulin = 0;
var baseBasalInsulin = 0;
var positiveTemps = 0;
var negativeTemps = 0;
iobpolyline += (xScale2(moment(from)) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' ';
cobpolyline += (xScale2(moment(from)) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' ';
var timestart = new Date();
var cobStatusAvailable = client.plugins('cob').isDeviceStatusAvailable(datastorage.devicestatus);
var iobStatusAvailable = client.plugins('iob').isDeviceStatusAvailable(datastorage.devicestatus);
console.log("Device COB status available: ", cobStatusAvailable);
console.log("Device IOB status available: ", iobStatusAvailable);
for (var dt = moment(from); dt < to; dt.add(5, 'minutes')) {
if (options.iob && !iobStatusAvailable) {
var iob = client.plugins('iob').calcTotal(datastorage.treatments, datastorage.devicestatus, profile, dt.toDate()).iob;
// make the graph discontinuous when data is missing
if (iob === undefined) {
iobpolyline += ', ' + (xScale2(lastDt) + padding.left) + ',' + (yInsulinScale(0) + padding.top);
iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(0) + padding.top);
} else {
if (lastIOB === undefined) {
iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(0) + padding.top);
}
iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(iob) + padding.top);
}
lastDt = dt.clone();
lastIOB = iob;
}
if (options.cob && !cobStatusAvailable) {
var cob = client.plugins('cob').cobTotal(datastorage.treatments, datastorage.devicestatus, profile, dt.toDate()).cob;
if (!dt.isSame(from)) {
cobpolyline += ', ';
}
cobpolyline += (xScale2(dt.toDate()) + padding.left) + ',' + (yCarbsScale(cob) + padding.top) + ' ';
}
if (options.basal) {
var date = dt.format('x');
var hournow = dt.hour();
var basalvalue = profile.getTempBasal(date);
// Calculate basal stats
baseBasalInsulin += basalvalue.basal * 5 / 60; // 5 minutes part
var tempPart = (basalvalue.tempbasal - basalvalue.basal) * 5 / 60;
if (tempPart > 0) {
positiveTemps += tempPart;
data.netBasalPositive[hournow] += tempPart;
}
if (tempPart < 0) {
negativeTemps += tempPart;
data.netBasalNegative[hournow] += tempPart;
}
if (!_.isEqual(lastbasal, basalvalue)) {
linedata.push({ d: date, b: basalvalue.totalbasal });
notemplinedata.push({ d: date, b: basalvalue.basal });
if (basalvalue.combobolustreatment && basalvalue.combobolustreatment.relative) {
tempbasalareadata.push({ d: date, b: basalvalue.tempbasal });
basalareadata.push({ d: date, b: 0 });
comboareadata.push({ d: date, b: basalvalue.totalbasal });
} else if (basalvalue.treatment) {
tempbasalareadata.push({ d: date, b: basalvalue.totalbasal });
basalareadata.push({ d: date, b: 0 });
comboareadata.push({ d: date, b: 0 });
} else {
tempbasalareadata.push({ d: date, b: 0 });
basalareadata.push({ d: date, b: basalvalue.totalbasal });
comboareadata.push({ d: date, b: 0 });
}
}
lastbasal = basalvalue;
}
}
// Draw COB from devicestatuses if available
if (cobStatusAvailable) {
var lastdate = 0;
var previousdate = 0;
var cobArray = client.plugins('cob').COBDeviceStatusesInTimeRange(datastorage.devicestatus, from.valueOf(), to.valueOf());
_.each(cobArray, function drawCob (point) {
if (previousdate !== 0 && point.mills - previousdate > times.mins(15).msecs) {
cobpolyline += ', ';
cobpolyline += (xScale2(previousdate) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' ';
cobpolyline += ', ';
cobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' ';
}
cobpolyline += ', ';
cobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yCarbsScale(point.cob) + padding.top) + ' ';
if (point.mills > lastdate) lastdate = point.mills;
previousdate = point.mills;
});
cobpolyline += ', ' + (xScale2(lastdate) + padding.left) + ',' + (yCarbsScale(0) + padding.top);
} else {
cobpolyline += ', ' + (xScale2(to) + padding.left) + ',' + (yCarbsScale(0) + padding.top);
}
// Draw IOB from devicestatuses if available
if (iobStatusAvailable) {
lastdate = 0;
previousdate = 0;
var iobArray = client.plugins('iob').IOBDeviceStatusesInTimeRange(datastorage.devicestatus, from.valueOf(), to.valueOf());
_.each(iobArray, function drawCob (point) {
if (previousdate !== 0 && point.mills - previousdate > times.mins(15).msecs) {
iobpolyline += ', ';
iobpolyline += (xScale2(previousdate) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' ';
iobpolyline += ', ';
iobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' ';
}
iobpolyline += ', ';
iobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yInsulinScale(point.iob) + padding.top) + ' ';
if (point.mills > lastdate) lastdate = point.mills;
previousdate = point.mills;
});
iobpolyline += ', ' + (xScale2(lastdate) + padding.left) + ',' + (yInsulinScale(0) + padding.top);
} else {
iobpolyline += ', ' + (xScale2(to) + padding.left) + ',' + (yInsulinScale(0) + padding.top);
}
if (options.iob) {
context.append('polyline')
.attr('stroke', 'blue')
.attr('opacity', '0.5')
.attr('fill-opacity', '0.1')
.attr('points', iobpolyline);
}
if (options.cob) {
context.append('polyline')
.attr('stroke', 'red')
.attr('opacity', '0.5')
.attr('fill-opacity', '0.1')
.attr('points', cobpolyline);
}
if (options.basal) {
var toTempBasal = profile.getTempBasal(to.format('x'));
linedata.push({ d: to.format('x'), b: toTempBasal.totalbasal });
notemplinedata.push({ d: to.format('x'), b: toTempBasal.basal });
basalareadata.push({ d: to.format('x'), b: toTempBasal.basal });
tempbasalareadata.push({ d: to.format('x'), b: toTempBasal.totalbasal });
comboareadata.push({ d: to.format('x'), b: toTempBasal.totalbasal });
var basalMax = d3.max(linedata, function(d) { return d.b; });
basalMax = Math.max(basalMax, d3.max(basalareadata, function(d) { return d.b; }));
basalMax = Math.max(basalMax, d3.max(tempbasalareadata, function(d) { return d.b; }));
basalMax = Math.max(basalMax, d3.max(comboareadata, function(d) { return d.b; }));
yScaleBasals.domain([basalMax, 0]);
var valueline = d3.line()
.curve(d3.curveStepAfter)
.x(function(d) { return xScale2(d.d) + padding.left; })
.y(function(d) { return yScaleBasals(d.b) + padding.top; });
var area = d3.area()
.curve(d3.curveStepAfter)
.x(function(d) { return xScale2(d.d) + padding.left; })
.y0(yScaleBasals(0) + padding.top)
.y1(function(d) { return yScaleBasals(d.b) + padding.top; });
var g = basals.append('g');
g.append('path')
.attr('class', 'line basalline')
.attr('stroke', '#0099ff')
.attr('stroke-width', 1)
.attr('fill', 'none')
.attr('d', valueline(linedata))
.attr('clip-path', 'url(#clip)');
g.append('path')
.attr('class', 'line notempline')
.attr('stroke', '#0099ff')
.attr('stroke-width', 1)
.attr('stroke-dasharray', ('3, 3'))
.attr('fill', 'none')
.attr('d', valueline(notemplinedata))
.attr('clip-path', 'url(#clip)');
g.append('path')
.attr('class', 'area basalarea')
.datum(basalareadata)
.attr('fill', '#0099ff')
.attr('fill-opacity', .1)
.attr('stroke-width', 0)
.attr('d', area);
g.append('path')
.attr('class', 'area tempbasalarea')
.datum(tempbasalareadata)
.attr('fill', '#0099ff')
.attr('fill-opacity', .3)
.attr('stroke-width', 1)
.attr('d', area);
g.append('path')
.attr('class', 'area comboarea')
.datum(comboareadata)
.attr('fill', 'url(#hash)')
.attr('fill-opacity', .3)
.attr('stroke-width', 1)
.attr('d', area);
datastorage.tempbasalTreatments.forEach(function(t) {
// only if basal and focus interval overlap and there is a chance to fit
if (t.mills < to.format('x') && t.mills + times.mins(t.duration).msecs > from.format('x')) {
var text = g.append('text')
.attr('class', 'tempbasaltext')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('fill', '#0099ff')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('x', xScale2((Math.max(t.mills, from.format('x')) + Math.min(t.mills + times.mins(t.duration).msecs, to.format('x'))) / 2) + padding.left)
.attr('y', yScaleBasals(0) - 10 + padding.top)
// .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (t.absolute ? Number(t.absolute).toFixed(2) + 'U' : ''));
// better hide if not fit
if (text.node().getBBox().width > xScale2(t.mills + times.mins(t.duration).msecs) - xScale2(t.mills)) {
text.attr('display', 'none');
}
}
});
}
data.treatments.forEach(function(treatment) {
// Calculate bolus stats
if (treatment.insulin) {
bolusInsulin += treatment.insulin;
}
// Combo bolus part
if (treatment.relative) {
bolusInsulin += treatment.relative;
}
var renderNotes = treatment.notes && options.notes;
if (treatment.eventType === 'Note' && !options.notes) return;
if (treatment.boluscalc && treatment.boluscalc.foods && treatment.boluscalc.foods.length > 0 || renderNotes) {
var lastfoodtext = foodtexts;
var drawpointer = false;
if (treatment.boluscalc && treatment.boluscalc.foods && treatment.boluscalc.foods.length > 0 && options.food) {
var foods = treatment.boluscalc.foods;
for (var fi = 0; fi < foods.length; fi++) {
var f = foods[fi];
var text = '' + f.name + ' ';
text += '' + (f.carbs * f.portions).toFixed(1) + ' g';
context.append('text')
.style('font-size', '10px')
.style('font-weight', 'normal')
.attr('fill', 'black')
.attr('y', foodtexts * 15 + padding.top)
.attr('transform', 'translate(' + (xScale2(treatment.mills) + padding.left) + ',' + padding.top + ')')
.html(text);
foodtexts = (foodtexts + 1) % 6;
drawpointer = true;
}
}
if (renderNotes && !treatment.duration && treatment.eventType !== 'Temp Basal') {
context.append('text')
.style('font-size', '10px')
.style('font-weight', 'normal')
.attr('fill', 'black')
.attr('y', foodtexts * 15 + padding.top)
.attr('transform', 'translate(' + (xScale2(treatment.mills) + padding.left) + ',' + padding.top + ')')
.html(treatment.notes);
foodtexts = (foodtexts + 1) % 6;
drawpointer = true;
}
if (drawpointer) {
context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale2(treatment.mills) + padding.left)
.attr('y1', lastfoodtext * 15 + padding.top)
.attr('x2', xScale2(treatment.mills) + padding.left)
.attr('y2', padding.top + treatment.carbs ? yCarbsScale(treatment.carbs) : (treatment.insulin ? yInsulinScale(treatment.insulin) : chartHeight))
.style('stroke-dasharray', ('1, 7'))
.attr('stroke', 'grey');
}
}
if (treatment.carbs && options.carbs) {
var label = ' ' + treatment.carbs + ' g';
label += treatment.foodType ? ' ' + treatment.foodType : ''
label += treatment.absorptionTime ? ' ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' : ''
if (treatment.protein) label += ' / ' + treatment.protein + ' g';
if (treatment.fat) label += ' / ' + treatment.fat + ' g';
context.append('rect')
.attr('y', yCarbsScale(treatment.carbs))
.attr('height', chartHeight - yCarbsScale(treatment.carbs))
.attr('width', 5)
.attr('stroke', 'red')
.attr('opacity', '0.5')
.attr('fill', 'red')
.attr('transform', 'translate(' + (xScale2(treatment.mills) + padding.left) + ',' + +padding.top + ')');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'red')
.attr('transform', 'rotate(-45,' + (xScale2(treatment.mills) + padding.left) + ',' + (padding.top + yCarbsScale(treatment.carbs)) + ') ' +
'translate(' + (xScale2(treatment.mills) + padding.left + 10) + ',' + (padding.top + yCarbsScale(treatment.carbs)) + ')')
.text('' + label);
}
if (treatment.insulin && options.insulin) {
var dataLabel = client.utils.toRoundedStr(treatment.insulin, 2)+ 'U';
context.append('rect')
.attr('y', yInsulinScale(treatment.insulin))
.attr('height', chartHeight - yInsulinScale(treatment.insulin))
.attr('width', 5)
.attr('stroke', 'blue')
.attr('opacity', '0.5')
.attr('fill', 'blue')
.attr('transform', 'translate(' + (xScale2(treatment.mills) + padding.left - 2) + ',' + +padding.top + ')');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'blue')
//.attr('y', yInsulinScale(treatment.insulin)-10)
.attr('transform', 'rotate(-45,' + (xScale2(treatment.mills) + padding.left - 2) + ',' + (padding.top + yInsulinScale(treatment.insulin)) + ')' +
'translate(' + (xScale2(treatment.mills) + padding.left + 10) + ',' + (padding.top + yInsulinScale(treatment.insulin)) + ')')
.text(dataLabel);
}
// process the rest
if (treatment.insulin || treatment.carbs || treatment.eventType == 'Temp Basal' || treatment.eventType == 'Combo Bolus') {
// ignore
} else if (treatment.eventType === 'Profile Switch') {
// profile switch
appendProfileSwitch(context, treatment);
if (treatment.duration && !treatment.cuttedby) {
appendProfileSwitch(context, {
cutting: treatment.profile
, profile: client.profilefunctions.activeProfileToTime(times.mins(treatment.duration).msecs + treatment.mills + 1)
, mills: times.mins(treatment.duration).msecs + treatment.mills
, end: true
});
}
} else if (treatment.eventType === 'Exercise' && treatment.duration) {
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
.attr('y', yScale2(client.utils.scaleMgdl(396)) + padding.top)
.attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills))
.attr('height', yScale2(client.utils.scaleMgdl(360)) - yScale2(client.utils.scaleMgdl(396)))
.attr('stroke-width', 1)
.attr('opacity', .2)
.attr('stroke', 'white')
.attr('fill', 'Violet');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'Violet')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('y', yScale2(client.utils.scaleMgdl(378)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.notes);
} else if (treatment.eventType === 'Note' && treatment.duration) {
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
.attr('y', yScale2(client.utils.scaleMgdl(360)) + padding.top)
.attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills))
.attr('height', yScale2(client.utils.scaleMgdl(324)) - yScale2(client.utils.scaleMgdl(360)))
.attr('stroke-width', 1)
.attr('opacity', .2)
.attr('stroke', 'white')
.attr('fill', 'Salmon');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'Salmon')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('y', yScale2(client.utils.scaleMgdl(342)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.notes);
} else if (treatment.eventType === 'OpenAPS Offline' && treatment.duration) {
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
.attr('y', yScale2(client.utils.scaleMgdl(324)) + padding.top)
.attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills))
.attr('height', yScale2(client.utils.scaleMgdl(288)) - yScale2(client.utils.scaleMgdl(324)))
.attr('stroke-width', 1)
.attr('opacity', .2)
.attr('stroke', 'white')
.attr('fill', 'Brown');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'Brown')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('y', yScale2(client.utils.scaleMgdl(306)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.notes);
} else if (treatment.eventType === 'Temporary Override' && treatment.duration ) {
// Loop Overrides with duration
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
.attr('y', yScale2(client.utils.scaleMgdl(432)) + padding.top)
.attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills))
.attr('height', yScale2(client.utils.scaleMgdl(396)) - yScale2(client.utils.scaleMgdl(432)))
.attr('stroke-width', 1)
.attr('opacity', .2)
.attr('stroke', 'white')
.attr('fill', 'black');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.reason);
} else if (treatment.eventType === 'BG Check' && !treatment.duration && options.bgcheck) {
context.append('circle')
.attr('cx', xScale2(treatment.mills) + padding.left)
.attr('cy', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top)
.attr('fill', 'red')
.style('opacity', 1)
.attr('stroke-width', 1)
.attr('stroke', 'darkred')
.attr('r', 4);
} else if (!treatment.duration && options.othertreatments) {
// other treatments without duration
context.append('circle')
.attr('cx', xScale2(treatment.mills) + padding.left)
.attr('cy', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top)
.attr('fill', 'purple')
.style('opacity', 1)
.attr('stroke-width', 1)
.attr('stroke', 'black')
.attr('r', 4);
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'purple')
.attr('y', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top - 10)
.attr('x', xScale2(treatment.mills) + padding.left + 10)
.text(translate(client.careportal.resolveEventName(treatment.eventType)));
} else if (treatment.duration && options.othertreatments) {
// other treatments with duration
context.append('rect')
.attr('x', xScale2(treatment.mills) + padding.left)
.attr('y', yScale2(client.utils.scaleMgdl(432)) + padding.top)
.attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills))
.attr('height', yScale2(client.utils.scaleMgdl(396)) - yScale2(client.utils.scaleMgdl(432)))
.attr('stroke-width', 1)
.attr('opacity', .2)
.attr('stroke', 'white')
.attr('fill', 'black');
context.append('text')
.style('font-size', '12px')
.style('font-weight', 'bold')
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top)
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
.text(treatment.notes);
} else {
console.log("missed treatment", treatment);
}
});
if (options.insulindistribution) {
var boluscolor = 'blue';
var basalcolor = '#0099ff';
console.log('Insulin for day: ' + day + ' bolus: ' + bolusInsulin + ' basebasal: ' + baseBasalInsulin + ' positiveTemps: ' + positiveTemps + ' negativeTemps: ' + negativeTemps);
var table = $('<table>');
$('<tr><td><b>' + translate('Bolus insulin:') + '</b></td><td align="right">' + bolusInsulin.toFixed(1) + 'U</td></tr>').appendTo(table);
if (positiveTemps > 0 || negativeTemps > 0) {
$('<tr><td>' + translate('Base basal insulin:') + '</td><td align="right">' + baseBasalInsulin.toFixed(1) + 'U</td></tr>').appendTo(table);
}
if (positiveTemps > 0) {
$('<tr><td>' + translate('Positive temp basal insulin:') + '</td><td align="right">' + positiveTemps.toFixed(1) + 'U</td></tr>').appendTo(table);
}
if (negativeTemps < 0) {
$('<tr><td>' + translate('Negative temp basal insulin:') + '</td><td align="right">' + negativeTemps.toFixed(1) + 'U</td></tr>').appendTo(table);
}
var totalBasalInsulin = baseBasalInsulin + positiveTemps + negativeTemps;
$('<tr><td><b>' + translate('Total basal insulin:') + '</b></td><td align="right">' + totalBasalInsulin.toFixed(1) + 'U</td></tr>').appendTo(table);
var totalDailyInsulin = bolusInsulin + baseBasalInsulin + positiveTemps + negativeTemps;
$('<tr><td><b>' + translate('Total daily insulin:') + '</b></td><td align="right">' + totalDailyInsulin.toFixed(1) + 'U</td></tr>').appendTo(table);
$('<tr><td>' + translate('Total carbs') + ':</td><td align="right">' + data.dailyCarbs + ' g</td></tr>').appendTo(table);
$('<tr><td>' + translate('Total protein') + ':</td><td align="right">' + data.dailyProtein + ' g</td></tr>').appendTo(table);
$('<tr><td>' + translate('Total fat') + ':</td><td align="right">' + data.dailyFat + ' g</td></tr>').appendTo(table);
$('<tr><td colspan="2"><span id="daytodaystatinsulinpiechart-' + day + '"></span><span id="daytodaystatcarbspiechart-' + day + '"></span></td></tr>').appendTo(table);
$('#daytodaystatchart-' + day).append(table);
var chartData = [
{
label: translate('Basal')
, count: totalBasalInsulin
, pct: (totalBasalInsulin / totalDailyInsulin * 100).toFixed(0)
}
, { label: translate('Bolus'), count: bolusInsulin, pct: (bolusInsulin / totalDailyInsulin * 100).toFixed(0) }
];
// Insulin distribution chart
var width = 120;
var height = 120;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal().range([basalcolor, boluscolor]);
var labelArc = d3.arc()
.outerRadius(radius / 2)
.innerRadius(radius / 2);
var svg = d3.select('#daytodaystatinsulinpiechart-' + day)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
var pie = d3.pie()
.value(function(d) {
return d.count;
})
.sort(null);
var insulg = svg.selectAll('.insulinarc')
.data(pie(chartData))
.enter()
.append('g')
.attr('class', 'insulinarc');
insulg.append('path')
.attr('d', arc)
.attr('opacity', '0.5')
.attr('fill', function(d) {
return color(d.data.label);
});
insulg.append('text')
.attr('transform', function(d) {
return 'translate(' + labelArc.centroid(d) + ')';
})
.attr('dy', '.15em')
.style('font-weight', 'bold')
.attr('text-anchor', 'middle')
.text(function(d) {
return d.data.pct + '%';
});
// Carbs pie chart
var carbscolor = d3.scaleOrdinal().range(['red']);
var carbsData = [
{ label: translate('Carbs'), count: data.dailyCarbs }
];
var carbssvg = d3.select('#daytodaystatcarbspiechart-' + day)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var carbsarc = d3.arc()
.outerRadius(radius * data.dailyCarbs / options.maxDailyCarbsValue);
var carbspie = d3.pie()
.value(function(d) {
return d.count;
})
.sort(null);
var carbsg = carbssvg.selectAll('.carbsarc')
.data(carbspie(carbsData))
.enter()
.append('g')
.attr('class', 'carbsarc');
carbsg.append('path')
.attr('d', carbsarc)
.attr('opacity', '0.5')
.attr('fill', function(d) {
return carbscolor(d.data.label);
});
carbsg.append('text')
.attr('transform', function() {
return 'translate(0,0)';
})
.attr('dy', '.15em')
.style('font-weight', 'bold')
.attr('text-anchor', 'middle')
.text(function(d) {
return d.data.count + 'g';
});
}
tddSum += totalDailyInsulin;
basalSum += totalBasalInsulin;
baseBasalSum += baseBasalInsulin;
bolusSum += bolusInsulin;
carbsSum += data.dailyCarbs;
proteinSum += data.dailyProtein;
fatSum += data.dailyFat;
appendProfileSwitch(context, {
//eventType: 'Profile Switch'
profile: client.profilefunctions.activeProfileToTime(from)
, mills: from + times.mins(15).msecs
, first: true
});
function appendProfileSwitch (context, treatment) {
if (!treatment.cutting && !treatment.profile) { return; }
var sign = treatment.first ? '▲▲▲' : '▬▬▬';
var text;
if (treatment.cutting) {
text = sign + ' ' + client.profilefunctions.profileSwitchName(treatment.cutting) + ' ' + '►►►' + ' ' + client.profilefunctions.profileSwitchName(treatment.profile) + ' ' + sign;
} else {
text = sign + ' ' + client.profilefunctions.profileSwitchName(treatment.profile) + ' ' + sign;
}
context.append('text')
.style('font-size', 12)
.style('font-weight', 'bold')
.attr('fill', '#0099ff')
.attr('text-anchor', 'start')
.attr('dy', '.35em')
.attr('transform', 'rotate(-90 ' + (xScale2(treatment.mills) + padding.left) + ',' + (yScaleBasals(0) + padding.top - 10) + ') ' +
'translate(' + (xScale2(treatment.mills) + padding.left) + ',' + (yScaleBasals(0) + padding.top - 10) + ')')
.text(text);
}
console.log("Rendering " + day, new Date().getTime() - timestart.getTime(), "msecs");
}
function hideTooltip () {
client.tooltip.style('opacity', 0);
}
};