Files
cgm-remote-monitor/static/js/client.js
T

1069 lines
42 KiB
JavaScript

(function () {
"use strict";
var latestSGV,
errorCode,
treatments,
padding = { top: 20, right: 10, bottom: 30, left: 10 },
opacity = {current: 1, DAY: 1, NIGHT: 0.5},
now = Date.now(),
data = [],
dateFn = function (d) { return new Date(d.date) },
xScale, xScale2, yScale, yScale2,
xAxis, yAxis, xAxis2, yAxis2,
prevChartWidth = 0,
prevChartHeight = 0,
focusHeight,
contextHeight,
UPDATE_TRANS_MS = 750, // milliseconds
brush,
BRUSH_TIMEOUT = 300000, // 5 minutes in ms
brushTimer,
brushInProgress = false,
clip,
TWENTY_FIVE_MINS_IN_MS = 1500000,
THIRTY_MINS_IN_MS = 1800000,
FORTY_MINS_IN_MS = 2400000,
FORTY_TWO_MINS_IN_MS = 2520000,
SIXTY_MINS_IN_MS = 3600000,
FOCUS_DATA_RANGE_MS = 12600000, // 3.5 hours of actual data
FORMAT_TIME = '%I:%M%', //alternate format '%H:%M'
audio = document.getElementById('audio'),
alarmInProgress = false,
currentAlarmType = null,
alarmSound = 'alarm.mp3',
urgentAlarmSound = 'alarm2.mp3',
WIDTH_TIME_HIDDEN = 600,
MINUTES_SINCE_LAST_UPDATE_WARN = 10,
MINUTES_SINCE_LAST_UPDATE_URGENT = 20;
// Tick Values
var tickValues = [40, 60, 80, 120, 180, 300, 400];
if (browserSettings.units == "mmol") {
var tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0];
}
//TODO: get these from the config
var targetTop = 180,
targetBottom = 80;
var futureOpacity = d3.scale.linear( )
.domain([TWENTY_FIVE_MINS_IN_MS, SIXTY_MINS_IN_MS])
.range([0.8, 0.1]);
// create svg and g to contain the chart contents
var charts = d3.select('#chartContainer').append('svg')
.append('g')
.attr('class', 'chartContainer')
.attr('transform', 'translate(' + padding.left + ',' + padding.top + ')');
var focus = charts.append('g');
// create the x axis container
focus.append('g')
.attr('class', 'x axis');
// create the y axis container
focus.append('g')
.attr('class', 'y axis');
var context = charts.append('g');
// create the x axis container
context.append('g')
.attr('class', 'x axis');
// create the y axis container
context.append('g')
.attr('class', 'y axis');
// Remove leading zeros from the time (eg. 08:40 = 8:40) & lowercase the am/pm
function formatTime(time) {
time = d3.time.format(FORMAT_TIME)(time);
time = time.replace(/^0/, '').toLowerCase();
return time;
}
// lixgbg: Convert mg/dL BG value to metric mmol
function scaleBg(bg) {
if (browserSettings.units == "mmol") {
return (Math.round((bg / 18) * 10) / 10).toFixed(1);
} else {
return bg;
}
}
// initial setup of chart when data is first made available
function initializeCharts() {
// define the parts of the axis that aren't dependent on width or height
xScale = d3.time.scale()
.domain(d3.extent(data, function (d) { return d.date; }));
yScale = d3.scale.log()
.domain([scaleBg(30), scaleBg(420)]);
xScale2 = d3.time.scale()
.domain(d3.extent(data, function (d) { return d.date; }));
yScale2 = d3.scale.log()
.domain([scaleBg(36), scaleBg(420)]);
xAxis = d3.svg.axis()
.scale(xScale)
.ticks(4)
.orient('top');
yAxis = d3.svg.axis()
.scale(yScale)
.tickFormat(d3.format('d'))
.tickValues(tickValues)
.orient('left');
xAxis2 = d3.svg.axis()
.scale(xScale2)
.ticks(4)
.orient('bottom');
yAxis2 = d3.svg.axis()
.scale(yScale2)
.tickFormat(d3.format('d'))
.tickValues(tickValues)
.orient('right');
// setup a brush
brush = d3.svg.brush()
.x(xScale2)
.on('brushstart', brushStarted)
.on('brush', brushed)
.on('brushend', brushEnded);
updateChart(true);
}
// get the desired opacity for context chart based on the brush extent
function highlightBrushPoints(data) {
if (data.date.getTime() >= brush.extent()[0].getTime() && data.date.getTime() <= brush.extent()[1].getTime()) {
return futureOpacity(data.date - latestSGV.x);
} else {
return 0.5;
}
}
// clears the current user brush and resets to the current real time data
function updateBrushToNow() {
// get current time range
var dataRange = d3.extent(data, dateFn);
// update brush and focus chart with recent data
d3.select('.brush')
.transition()
.duration(UPDATE_TRANS_MS)
.call(brush.extent([new Date(dataRange[1].getTime() - FOCUS_DATA_RANGE_MS), dataRange[1]]));
brushed(true);
// clear user brush tracking
brushInProgress = false;
}
function brushStarted() {
// update the opacity of the context data points to brush extent
context.selectAll('circle')
.data(data)
.style('opacity', function (d) { return 1; });
}
function brushEnded() {
// update the opacity of the context data points to brush extent
context.selectAll('circle')
.data(data)
.style('opacity', function (d) { return highlightBrushPoints(d) });
}
// function to call when context chart is brushed
function brushed(skipTimer) {
if (!skipTimer) {
// set a timer to reset focus chart to real-time data
clearTimeout(brushTimer);
brushTimer = setTimeout(updateBrushToNow, BRUSH_TIMEOUT);
brushInProgress = true;
}
var brushExtent = brush.extent();
// ensure that brush extent is fixed at 3.5 hours
if (brushExtent[1].getTime() - brushExtent[0].getTime() != FOCUS_DATA_RANGE_MS) {
// ensure that brush updating is with the time range
if (brushExtent[0].getTime() + FOCUS_DATA_RANGE_MS > d3.extent(data, dateFn)[1].getTime()) {
brushExtent[0] = new Date(brushExtent[1].getTime() - FOCUS_DATA_RANGE_MS);
d3.select('.brush')
.call(brush.extent([brushExtent[0], brushExtent[1]]));
} else {
brushExtent[1] = new Date(brushExtent[0].getTime() + FOCUS_DATA_RANGE_MS);
d3.select('.brush')
.call(brush.extent([brushExtent[0], brushExtent[1]]));
}
}
// get slice of data so that concatenation of predictions do not interfere with subsequent updates
var focusData = data.slice();
if (alarmInProgress) {
if ($(window).width() > WIDTH_TIME_HIDDEN) {
$(".time").show();
} else {
$(".time").hide();
}
}
var element = document.getElementById('bgButton').hidden == '';
var nowDate = new Date(brushExtent[1] - THIRTY_MINS_IN_MS);
// predict for retrospective data
if (brushExtent[1].getTime() - THIRTY_MINS_IN_MS < now && element != true) {
// filter data for -12 and +5 minutes from reference time for retrospective focus data prediction
var nowData = data.filter(function(d) {
return d.date.getTime() >= brushExtent[1].getTime() - FORTY_TWO_MINS_IN_MS &&
d.date.getTime() <= brushExtent[1].getTime() - TWENTY_FIVE_MINS_IN_MS &&
d.color != 'none';
});
if (nowData.length > 1) {
var prediction = predictAR(nowData);
focusData = focusData.concat(prediction);
var focusPoint = nowData[nowData.length - 1];
$('.container .currentBG')
.text(focusPoint.sgv)
.css('text-decoration','line-through');
$('.container .currentDirection')
.html(focusPoint.direction)
} else {
$('.container .currentBG')
.text("---")
.css('text-decoration','');
}
$('#currentTime')
.text(formatTime(new Date(brushExtent[1] - THIRTY_MINS_IN_MS)))
.css('text-decoration','line-through');
$('#lastEntry').text("RETRO").removeClass('current');
$('.container #noButton .currentBG').css({color: 'grey'});
$('.container #noButton .currentDirection').css({color: 'grey'});
} else {
// if the brush comes back into the current time range then it should reset to the current time and sg
var nowData = data.filter(function(d) {
return d.color != 'none';
});
nowData = [nowData[nowData.length - 2], nowData[nowData.length - 1]];
var prediction = predictAR(nowData);
focusData = focusData.concat(prediction);
var dateTime = new Date(now);
nowDate = dateTime;
$('#currentTime')
.text(formatTime(dateTime))
.css('text-decoration', '');
if (errorCode) {
var errorDisplay;
switch (parseInt(errorCode)) {
case 0: errorDisplay = '??0'; break; //None
case 1: errorDisplay = '?SN'; break; //SENSOR_NOT_ACTIVE
case 2: errorDisplay = '??2'; break; //MINIMAL_DEVIATION
case 3: errorDisplay = '?NA'; break; //NO_ANTENNA
case 5: errorDisplay = '?NC'; break; //SENSOR_NOT_CALIBRATED
case 6: errorDisplay = '?CD'; break; //COUNTS_DEVIATION
case 7: errorDisplay = '??7'; break; //?
case 8: errorDisplay = '??8'; break; //?
case 9: errorDisplay = '&#8987;'; break; //ABSOLUTE_DEVIATION
case 10: errorDisplay = '???'; break; //POWER_DEVIATION
case 12: errorDisplay = '?RF'; break; //BAD_RF
default: errorDisplay = '?' + parseInt(errorCode) + '?'; break;
}
$('#lastEntry').text("CGM ERROR").removeClass('current').addClass("urgent");
$('.container .currentBG').html(errorDisplay)
.css('text-decoration', '');
$('.container .currentDirection').html('✖');
var color = sgvToColor(errorCode);
$('.container #noButton .currentBG').css({color: color});
$('.container #noButton .currentDirection').css({color: color});
} else {
var secsSinceLast = (Date.now() - new Date(latestSGV.x).getTime()) / 1000;
$('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60);
$('.container .currentBG')
.text(scaleBg(latestSGV.y))
.css('text-decoration', '');
$('.container .currentDirection')
.html(latestSGV.direction);
var color = sgvToColor(latestSGV.y);
$('.container #noButton .currentBG').css({color: color});
$('.container #noButton .currentDirection').css({color: color});
}
}
xScale.domain(brush.extent());
// bind up the focus chart data to an array of circles
// selects all our data into data and uses date function to get current max date
var focusCircles = focus.selectAll('circle').data(focusData, dateFn);
// if already existing then transition each circle to its new position
focusCircles
.transition()
.duration(UPDATE_TRANS_MS)
.attr('cx', function (d) { return xScale(d.date); })
.attr('cy', function (d) { return yScale(d.sgv); })
.attr('fill', function (d) { return d.color; });
// if new circle then just display
focusCircles.enter().append('circle')
.attr('cx', function (d) { return xScale(d.date); })
.attr('cy', function (d) { return yScale(d.sgv); })
.attr('fill', function (d) { return d.color; })
.attr('opacity', function (d) { return futureOpacity(d.date - latestSGV.x); })
.attr('r', 3);
focusCircles.exit()
.remove();
// remove all insulin/carb treatment bubbles so that they can be redrawn to correct location
d3.selectAll('.path').remove();
// add treatment bubbles
var bubbleSize = prevChartWidth < 400 ? 4 : (prevChartWidth < 600 ? 3 : 2);
focus.selectAll('circle')
.data(treatments)
.each(function (d) { drawTreatment(d, bubbleSize, true) });
// transition open-top line to correct location
focus.select('.open-top')
.attr('x1', xScale2(brush.extent()[0]))
.attr('y1', yScale(scaleBg(30)))
.attr('x2', xScale2(brush.extent()[1]))
.attr('y2', yScale(scaleBg(30)));
// transition open-left line to correct location
focus.select('.open-left')
.attr('x1', xScale2(brush.extent()[0]))
.attr('y1', focusHeight)
.attr('x2', xScale2(brush.extent()[0]))
.attr('y2', prevChartHeight);
// transition open-right line to correct location
focus.select('.open-right')
.attr('x1', xScale2(brush.extent()[1]))
.attr('y1', focusHeight)
.attr('x2', xScale2(brush.extent()[1]))
.attr('y2', prevChartHeight);
focus.select('.now-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale(nowDate))
.attr('y1', yScale(scaleBg(36)))
.attr('x2', xScale(nowDate))
.attr('y2', yScale(scaleBg(420)));
// update x axis
focus.select('.x.axis')
.call(xAxis);
// add clipping path so that data stays within axis
focusCircles.attr('clip-path', 'url(#clip)');
}
// called for initial update and updates for resize
function updateChart(init) {
// get current data range
var dataRange = d3.extent(data, dateFn);
// get the entire container height and width subtracting the padding
var chartWidth = (document.getElementById('chartContainer')
.getBoundingClientRect().width) - padding.left - padding.right;
var chartHeight = (document.getElementById('chartContainer')
.getBoundingClientRect().height) - padding.top - padding.bottom;
// get the height of each chart based on its container size ratio
focusHeight = chartHeight * .7;
contextHeight = chartHeight * .2;
// get current brush extent
var currentBrushExtent = brush.extent();
// only redraw chart if chart size has changed
if ((prevChartWidth != chartWidth) || (prevChartHeight != chartHeight)) {
prevChartWidth = chartWidth;
prevChartHeight = chartHeight;
//set the width and height of the SVG element
charts.attr('width', chartWidth + padding.left + padding.right)
.attr('height', chartHeight + padding.top + padding.bottom);
// ranges are based on the width and height available so reset
xScale.range([0, chartWidth]);
xScale2.range([0, chartWidth]);
yScale.range([focusHeight, 0]);
yScale2.range([chartHeight, chartHeight - contextHeight]);
if (init) {
// if first run then just display axis with no transition
focus.select('.x')
.attr('transform', 'translate(0,' + focusHeight + ')')
.call(xAxis);
focus.select('.y')
.attr('transform', 'translate(' + chartWidth + ',0)')
.call(yAxis);
// if first run then just display axis with no transition
context.select('.x')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis2);
context.append('g')
.attr('class', 'x brush')
.call(d3.svg.brush().x(xScale2).on('brush', brushed))
.selectAll('rect')
.attr('y', focusHeight)
.attr('height', chartHeight - focusHeight);
// disable resizing of brush
d3.select('.x.brush').select('.background').style('cursor', 'move');
d3.select('.x.brush').select('.resize.e').style('cursor', 'move');
d3.select('.x.brush').select('.resize.w').style('cursor', 'move');
// create a clipPath for when brushing
clip = charts.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('height', chartHeight)
.attr('width', chartWidth);
// add a line that marks the current time
focus.append('line')
.attr('class', 'now-line')
.attr('x1', xScale(new Date(now)))
.attr('y1', yScale(scaleBg(36)))
.attr('x2', xScale(new Date(now)))
.attr('y2', yScale(scaleBg(420)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
// add a y-axis line that shows the high bg threshold
focus.append('line')
.attr('class', 'high-line')
.attr('x1', xScale(dataRange[0]))
.attr('y1', yScale(scaleBg(180)))
.attr('x2', xScale(dataRange[1]))
.attr('y2', yScale(scaleBg(180)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
// add a y-axis line that shows the low bg threshold
focus.append('line')
.attr('class', 'low-line')
.attr('x1', xScale(dataRange[0]))
.attr('y1', yScale(scaleBg(80)))
.attr('x2', xScale(dataRange[1]))
.attr('y2', yScale(scaleBg(80)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
// add a y-axis line that opens up the brush extent from the context to the focus
focus.append('line')
.attr('class', 'open-top')
.attr('stroke', 'black')
.attr('stroke-width', 2);
// add a x-axis line that closes the the brush container on left side
focus.append('line')
.attr('class', 'open-left')
.attr('stroke', 'white');
// add a x-axis line that closes the the brush container on right side
focus.append('line')
.attr('class', 'open-right')
.attr('stroke', 'white');
// add a line that marks the current time
context.append('line')
.attr('class', 'now-line')
.attr('x1', xScale(new Date(now)))
.attr('y1', yScale2(scaleBg(36)))
.attr('x2', xScale(new Date(now)))
.attr('y2', yScale2(scaleBg(420)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
// add a y-axis line that shows the high bg threshold
context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale(dataRange[0]))
.attr('y1', yScale2(scaleBg(180)))
.attr('x2', xScale(dataRange[1]))
.attr('y2', yScale2(scaleBg(180)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
// add a y-axis line that shows the low bg threshold
context.append('line')
.attr('class', 'low-line')
.attr('x1', xScale(dataRange[0]))
.attr('y1', yScale2(scaleBg(80)))
.attr('x2', xScale(dataRange[1]))
.attr('y2', yScale2(scaleBg(80)))
.style('stroke-dasharray', ('3, 3'))
.attr('stroke', 'grey');
} else {
// for subsequent updates use a transition to animate the axis to the new position
var focusTransition = focus.transition().duration(UPDATE_TRANS_MS);
focusTransition.select('.x')
.attr('transform', 'translate(0,' + focusHeight + ')')
.call(xAxis);
focusTransition.select('.y')
.attr('transform', 'translate(' + chartWidth + ', 0)')
.call(yAxis);
var contextTransition = context.transition().duration(UPDATE_TRANS_MS);
contextTransition.select('.x')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis2);
// reset clip to new dimensions
clip.transition()
.attr('width', chartWidth)
.attr('height', chartHeight);
// reset brush location
context.select('.x.brush')
.selectAll('rect')
.attr('y', focusHeight)
.attr('height', chartHeight - focusHeight);
// clear current brush
d3.select('.brush').call(brush.clear());
// redraw old brush with new dimensions
d3.select('.brush').transition().duration(UPDATE_TRANS_MS).call(brush.extent(currentBrushExtent));
// transition high line to correct location
focus.select('.high-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale(currentBrushExtent[0]))
.attr('y1', yScale(scaleBg(180)))
.attr('x2', xScale(currentBrushExtent[1]))
.attr('y2', yScale(scaleBg(180)));
// transition low line to correct location
focus.select('.low-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale(currentBrushExtent[0]))
.attr('y1', yScale(scaleBg(80)))
.attr('x2', xScale(currentBrushExtent[1]))
.attr('y2', yScale(scaleBg(80)));
// transition open-top line to correct location
focus.select('.open-top')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(currentBrushExtent[0]))
.attr('y1', yScale(scaleBg(30)))
.attr('x2', xScale2(currentBrushExtent[1]))
.attr('y2', yScale(scaleBg(30)));
// transition open-left line to correct location
focus.select('.open-left')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(currentBrushExtent[0]))
.attr('y1', focusHeight)
.attr('x2', xScale2(currentBrushExtent[0]))
.attr('y2', chartHeight);
// transition open-right line to correct location
focus.select('.open-right')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(currentBrushExtent[1]))
.attr('y1', focusHeight)
.attr('x2', xScale2(currentBrushExtent[1]))
.attr('y2', chartHeight);
// transition high line to correct location
context.select('.high-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(dataRange[0]))
.attr('y1', yScale2(scaleBg(targetTop)))
.attr('x2', xScale2(dataRange[1]))
.attr('y2', yScale2(scaleBg(targetTop)));
// transition low line to correct location
context.select('.low-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(dataRange[0]))
.attr('y1', yScale2(scaleBg(targetBottom)))
.attr('x2', xScale2(dataRange[1]))
.attr('y2', yScale2(scaleBg(targetBottom)));
}
}
// update domain
xScale2.domain(dataRange);
context.select('.now-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale2(new Date(now)))
.attr('y1', yScale2(scaleBg(36)))
.attr('x2', xScale2(new Date(now)))
.attr('y2', yScale2(scaleBg(420)));
// only if a user brush is not active, update brush and focus chart with recent data
// else, just transition brush
var updateBrush = d3.select('.brush').transition().duration(UPDATE_TRANS_MS);
if (!brushInProgress) {
updateBrush
.call(brush.extent([new Date(dataRange[1].getTime() - FOCUS_DATA_RANGE_MS), dataRange[1]]));
brushed(true);
} else {
updateBrush
.call(brush.extent([currentBrushExtent[0], currentBrushExtent[1]]));
brushed(true);
}
// bind up the context chart data to an array of circles
var contextCircles = context.selectAll('circle')
.data(data);
// if already existing then transition each circle to its new position
contextCircles.transition()
.duration(UPDATE_TRANS_MS)
.attr('cx', function (d) { return xScale2(d.date); })
.attr('cy', function (d) { return yScale2(d.sgv); })
.attr('fill', function (d) { return d.color; })
.style('opacity', function (d) { return highlightBrushPoints(d) });
// if new circle then just display
contextCircles.enter().append('circle')
.attr('cx', function (d) { return xScale2(d.date); })
.attr('cy', function (d) { return yScale2(d.sgv); })
.attr('fill', function (d) { return d.color; })
.style('opacity', function (d) { return highlightBrushPoints(d) })
.attr('r', 2);
contextCircles.exit()
.remove();
// update x axis domain
context.select('.x')
.call(xAxis2);
}
// look for resize but use timer to only call the update script when a resize stops
var resizeTimer;
window.onresize = function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
updateChart(false);
}, 100);
};
var silenceDropdown = new Dropdown(".dropdown-menu");
$('#bgButton').click(function (e) {
silenceDropdown.open(e);
});
$("#silenceBtn").find("a").click(function () {
stopAlarm(true, $(this).data("snooze-time"));
});
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Client-side code to connect to server and handle incoming data
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var isInitialData = false;
var socket = io.connect();
socket.on('now', function (d) {
now = d;
var dateTime = new Date(now);
$('#currentTime').text(formatTime(dateTime));
// Dim the screen by reducing the opacity when at nighttime
if (browserSettings.nightMode) {
if (opacity.current != opacity.NIGHT && (dateTime.getHours() > 21 || dateTime.getHours() < 7)) {
$('body').css({ 'opacity': opacity.NIGHT });
} else {
$('body').css({ 'opacity': opacity.DAY });
}
}
});
socket.on('sgv', function (d) {
if (d.length > 1) {
errorCode = d.length >= 5 ? d[4] : undefined;
// change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr)
if (d[0].length) {
latestSGV = d[0][d[0].length - 1];
//TODO: alarmHigh/alarmLow probably shouldn't be here
if (browserSettings.alarmHigh) {
$('.container .current').toggleClass('high', latestSGV.y > 180);
}
if (browserSettings.alarmLow) {
$('.container .current').toggleClass('low', latestSGV.y < 70);
}
}
data = d[0].map(function (obj) {
return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: sgvToColor(obj.y)}
});
// TODO: This is a kludge to advance the time as data becomes stale by making old predictor clear (using color = 'none')
// This shouldn't have to be sent and can be fixed by using xScale.domain([x0,x1]) function with
// 2 days before now as x0 and 30 minutes from now for x1 for context plot, but this will be
// required to happen when "now" event is sent from websocket.js every minute. When fixed,
// remove all "color != 'none'" code
data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'none'} }));
data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'red'} }));
data.forEach(function (d) {
if (d.sgv < 39)
d.color = "transparent";
})
treatments = d[3];
if (!isInitialData) {
isInitialData = true;
initializeCharts();
}
else {
updateChart(false);
}
}
});
function sgvToColor(sgv) {
var color = 'grey';
if (browserSettings.theme == "colors") {
if (sgv > targetTop) {
color = 'yellow';
} else if (sgv >= targetBottom && sgv <= targetTop) {
color = '#4cff00';
} else if (sgv < targetBottom) {
color = 'red';
}
}
return color;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Alarms and Text handling
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
socket.on('connect', function () {
console.log('Client connected to server.')
});
socket.on('alarm', function () {
if (browserSettings.alarmHigh) {
console.log("Alarm raised!");
currentAlarmType = 'alarm';
generateAlarm(alarmSound);
}
brushInProgress = false;
updateChart(false);
});
socket.on('urgent_alarm', function () {
if (browserSettings.alarmLow) {
console.log("Urgent alarm raised!");
currentAlarmType = 'urgent_alarm';
generateAlarm(urgentAlarmSound);
}
brushInProgress = false;
updateChart(false);
});
socket.on('clear_alarm', function () {
if (alarmInProgress) {
console.log('clearing alarm');
stopAlarm();
}
});
$('#testAlarms').click(function(event) {
d3.select('.audio.alarms audio').each(function (data, i) {
var audio = this;
playAlarm(audio);
setTimeout(function() {
audio.pause();
}, 4000);
});
event.preventDefault();
});
function generateAlarm(file) {
alarmInProgress = true;
var selector = '.audio.alarms audio.' + file;
d3.select(selector).each(function (d, i) {
var audio = this;
playAlarm(audio);
$(this).addClass('playing');
});
var element = document.getElementById('bgButton');
element.hidden = '';
var element1 = document.getElementById('noButton');
element1.hidden = 'true';
$('.container .currentBG').text();
if ($(window).width() <= WIDTH_TIME_HIDDEN) {
$(".time").hide();
}
}
function playAlarm(audio) {
// ?mute=true disables alarms to testers.
if (querystring.mute != "true") {
audio.play();
} else {
showNotification("Alarm is muted per your request. (?mute=true)");
}
}
function stopAlarm(isClient, silenceTime) {
alarmInProgress = false;
var element = document.getElementById('bgButton');
element.hidden = 'true';
element = document.getElementById('noButton');
element.hidden = '';
d3.select('audio.playing').each(function (d, i) {
var audio = this;
audio.pause();
$(this).removeClass('playing');
});
$(".time").show();
// only emit ack if client invoke by button press
if (isClient) {
socket.emit('ack', currentAlarmType || 'alarm', silenceTime);
brushed(false);
}
}
function timeAgo(offset) {
var parts = {},
MINUTE = 60,
HOUR = 3600,
DAY = 86400,
WEEK = 604800;
//offset = (MINUTE * MINUTES_SINCE_LAST_UPDATE_WARN) + 60
//offset = (MINUTE * MINUTES_SINCE_LAST_UPDATE_URGENT) + 60
if (offset <= MINUTE) parts = { label: 'now' };
if (offset <= MINUTE * 2) parts = { label: '1 min ago' };
else if (offset < (MINUTE * 60)) parts = { value: Math.round(Math.abs(offset / MINUTE)), label: 'mins' };
else if (offset < (HOUR * 2)) parts = { label: '1 hr ago' };
else if (offset < (HOUR * 24)) parts = { value: Math.round(Math.abs(offset / HOUR)), label: 'hrs' };
else if (offset < DAY) parts = { label: '1 day ago' };
else if (offset < (DAY * 7)) parts = { value: Math.round(Math.abs(offset / DAY)), label: 'day' };
else if (offset < (WEEK * 52)) parts = { value: Math.round(Math.abs(offset / WEEK)), label: 'week' };
else parts = { label: 'a long time ago' };
if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_URGENT)) {
var lastEntry = $("#lastEntry");
lastEntry.removeClass("warn");
lastEntry.addClass("urgent");
$(".bgStatus").removeClass("current");
} else if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_WARN)) {
var lastEntry = $("#lastEntry");
lastEntry.removeClass("urgent");
lastEntry.addClass("warn");
} else {
$(".bgStatus").addClass("current");
$("#lastEntry").removeClass("warn urgent");
}
if (parts.value)
return parts.value + ' ' + parts.label + ' ago';
else
return parts.label;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//draw a compact visualization of a treatment (carbs, insulin)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function drawTreatment(treatment, scale, showValues) {
if (!treatment.CR) {
//plot a simple treatment point
console.info("plotting treatment", treatment);
var treatmentDots = focus.selectAll('treatment-dot')
.data(treatment)
.enter()
.append('g')
.attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(300)) + ')');
//TODO: some d3 magic to get the treatments to display and show a tooltip
} else {
var carbs = treatment.carbs;
var insulin = treatment.insulin;
var CR = treatment.CR;
var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale,
R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale,
R3 = R2 + 8 / scale;
var arc_data = [
{ 'element': '', 'color': '#9c4333', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 },
{ 'element': '', 'color': '#d4897b', 'start': -1.5708, 'end': 1.5708, 'inner': R1, 'outer': R2 },
{ 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 },
{ 'element': '', 'color': '#3d53b7', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 },
{ 'element': '', 'color': '#5d72c9', 'start': 1.5708, 'end': 4.7124, 'inner': R1, 'outer': R2 },
{ 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': R2, 'outer': R3 }
];
if (carbs < insulin * CR) arc_data[1].color = 'transparent';
if (carbs > insulin * CR) arc_data[4].color = 'transparent';
if (carbs > 0) arc_data[2].element = Math.round(carbs) + ' g';
if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U';
var arc = d3.svg.arc()
.innerRadius(function (d) {
return 5 * d.inner;
})
.outerRadius(function (d) {
return 5 * d.outer;
})
.endAngle(function (d) {
return d.start;
})
.startAngle(function (d) {
return d.end;
});
var treatmentDots = focus.selectAll('treatment-dot')
.data(arc_data)
.enter()
.append('g')
.attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')');
var arcs = treatmentDots.append('path')
.attr('class', 'path')
.attr('fill', function (d, i) {
return d.color;
})
.attr('id', function (d, i) {
return 's' + i;
})
.attr('d', arc);
// labels for carbs and insulin
if (showValues) {
var label = treatmentDots.append('g')
.attr('class', 'path')
.attr('id', 'label')
.style('fill', 'white');
label.append('text')
.style('font-size', 30 / scale)
.style('font-family', 'Arial')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('transform', function (d) {
d.outerRadius = d.outerRadius * 2.1;
d.innerRadius = d.outerRadius * 2.1;
return 'translate(' + arc.centroid(d) + ')';
})
.text(function (d) {
return d.element;
})
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// function to predict
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function predictAR(actual) {
var ONE_MINUTE = 60 * 1000;
var FIVE_MINUTES = 5 * ONE_MINUTE;
var predicted = [];
var BG_REF = 140;
var BG_MIN = 36;
var BG_MAX = 400;
// these are the one sigma limits for the first 13 prediction interval uncertainties (65 minutes)
var CONE = [0.020, 0.041, 0.061, 0.081, 0.099, 0.116, 0.132, 0.146, 0.159, 0.171, 0.182, 0.192, 0.201];
if (actual.length < 2) {
var y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)];
} else {
var elapsedMins = (actual[1].date - actual[0].date) / ONE_MINUTE;
if (elapsedMins < 5.1) {
y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[1].sgv / BG_REF)];
} else {
y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)];
}
}
var AR = [-0.723, 1.716];
var dt = actual[1].date.getTime();
var predictedColor = 'blue';
if (browserSettings.theme == "colors") {
predictedColor = 'cyan';
}
for (var i = 0; i < CONE.length; i++) {
y = [y[1], AR[0] * y[0] + AR[1] * y[1]];
dt = dt + FIVE_MINUTES;
// Add 2000 ms so not same point as SG
predicted[i * 2] = {
date: new Date(dt + 2000),
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] - 2 * CONE[i]))))),
color: predictedColor
};
// Add 4000 ms so not same point as SG
predicted[i * 2 + 1] = {
date: new Date(dt + 4000),
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] + 2 * CONE[i]))))),
color: predictedColor
};
predicted.forEach(function (d) {
if (d.sgv < BG_MIN)
d.color = "transparent";
})
}
return predicted;
}
})();