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.
327 lines (271 loc) • 11.6 kB
JavaScript
;
var _ = require('lodash');
var moment = window.moment;
var d3 = (global && global.d3) || require('d3');
var dayColors = [
'rgb(73, 22, 153)'
, 'rgb(34, 201, 228)'
, 'rgb(0, 153, 123)'
, 'rgb(135, 135, 228)'
, 'rgb(135, 49, 204)'
, 'rgb(36, 36, 228)'
, 'rgb(0, 234, 188)'
];
var weektoweek = {
name: 'weektoweek'
, label: 'Week to week'
, pluginType: 'report'
};
function init (ctx) {
weektoweek.html = function html (client) {
var translate = client.translate;
var ret =
'<h2>' + translate('Week to week') + '</h2>' +
'<b>' + translate('To see this report, press SHOW while in this view') + '</b><br>' +
' ' + translate('Size') +
' <select id="wrp_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>' +
'</select>' +
'<br>' +
translate('Scale') + ': ' +
'<input type="radio" name="wrp_scale" id="wrp_linear" checked>' +
translate('Linear') +
'<input type="radio" name="wrp_scale" id="wrp_log">' +
translate('Logarithmic') +
'<br>' +
'<div id="weektoweekcharts">' +
'</div>';
return ret;
};
weektoweek.prepareHtml = function weektoweekPrepareHtml (weekstoshow) {
$('#weektoweekcharts').html('');
var translate = ctx.language.translate;
var colorIdx = 0;
var legend = '<table>';
legend += '<tr><td><svg width="16" height="16"><g><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Sunday') + '</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Monday') + '</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Tuesday') + '</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Wednesday') + '</td></tr>';
legend += '<tr><td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Thursday') + '</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Friday') + '</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>' + translate('Saturday') + '</td></tr>';
legend += '</table>';
$('#weektoweekcharts').append($(legend));
weekstoshow.forEach(function eachWeek (d) {
$('#weektoweekcharts').append($('<table><tr><td><div id="weektoweekchart-' + d[0] + '-' + d[d.length - 1] + '"></div></td><td><div id="weektoweekstatchart-' + d[0] + '-' + d[d.length - 1] + '"></td></tr></table>'));
});
};
weektoweek.report = function report_weektoweek (datastorage, sorteddaystoshow, options) {
var Nightscout = window.Nightscout;
var client = Nightscout.client;
var report_plugins = Nightscout.report_plugins;
var padding = { top: 15, right: 22, bottom: 30, left: 35 };
var weekstoshow = [];
var startDay = moment(sorteddaystoshow[0] + ' 00:00:00');
sorteddaystoshow.forEach(function eachDay (day) {
var weekNum = Math.abs(moment(day + ' 00:00:00').diff(startDay, 'weeks'));
if (typeof weekstoshow[weekNum] === 'undefined') {
weekstoshow[weekNum] = [];
}
weekstoshow[weekNum].push(day);
});
weekstoshow = weekstoshow.map(function orderWeek (week) {
return _.sortBy(week);
});
weektoweek.prepareHtml(weekstoshow);
weekstoshow.forEach(function eachWeek (week) {
var sgvData = [];
var weekStart = moment(week[0] + ' 00:00:00');
week.forEach(function eachDay (day) {
var dayNum = Math.abs(moment(day + ' 00:00:00').diff(weekStart, 'days'));
datastorage[day].sgv.forEach(function eachSgv (sgv) {
var sgvWeekday = moment(sgv.date).day();
var sgvColor = dayColors[sgvWeekday];
if (sgv.color === 'gray') {
sgvColor = sgv.color;
}
sgvData.push({
'color': sgvColor
, 'date': moment(sgv.date).subtract(dayNum, 'days').toDate()
, 'filtered': sgv.filtered
, 'mills': sgv.mills - dayNum * 24 * 60 * 60000
, 'noise': sgv.noise
, 'sgv': sgv.sgv
, 'type': sgv.type
, 'unfiltered': sgv.unfiltered
, 'y': sgv.y
});
});
});
drawChart(week, sgvData, options);
});
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 (week, sgvData, options) {
var tickValues
, charts
, context
, xScale2, yScale2
, xAxis2, yAxis2
, dateFn = function(d) { return new Date(d.date); };
tickValues = client.ticks(client, {
scaleY: options.weekscale === 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('#weektoweekchart-' + week[0] + '-' + week[week.length - 1]).html(
'<b>' +
report_plugins.utils.localeDate(week[0]) +
'-' +
report_plugins.utils.localeDate(week[week.length - 1]) +
'</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(sgvData, dateFn));
if (options.weekscale === report_plugins.consts.SCALE_LOG) {
yScale2 = d3.scaleLog()
.domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]);
} else {
yScale2 = d3.scaleLinear()
.domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]);
}
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(sgvData, dateFn);
// get the entire container height and width subtracting the padding
var chartWidth = options.weekwidth - padding.left - padding.right;
var chartHeight = options.weekheight - padding.top - padding.bottom;
//set the width and height of the SVG element
charts.attr('width', options.weekwidth)
.attr('height', options.weekheight);
// ranges are based on the width and height available so reset
xScale2.range([0, chartWidth]);
yScale2.range([chartHeight, 0]);
// 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');
});
// bind up the context chart data to an array of circles
var contextCircles = context.selectAll('circle')
.data(sgvData);
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') {
return 'transparent';
}
return d.color;
})
.style('opacity', function() { return 0.5 })
.attr('stroke-width', function(d) { if (d.type === 'mbg') { return 2; } else { return 0; } })
.attr('stroke', function() { return 'black'; })
.attr('r', function(d) {
if (d.type === 'mbg') {
return 4;
} else {
return 2 + (options.weekwidth - 800) / 400;
}
})
.on('mouseout', hideTooltip);
if (badData.length > 0) {
console.warn('Bad Data: isNaN(sgv)', badData);
}
return sel;
}
// if new circle then just display
prepareContextCircles(contextCircles.enter().append('circle'));
contextCircles.exit()
.remove();
}
function hideTooltip () {
client.tooltip.style('opacity', 0);
}
};
return weektoweek;
}
module.exports = init;