UNPKG

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
'use strict'; 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>' + '&nbsp;' + 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;