Merge branch 'dev' of github.com:nightscout/cgm-remote-monitor into wip/push-notify

This commit is contained in:
Jason Calabrese
2015-01-06 00:22:28 -08:00
9 changed files with 328 additions and 212 deletions
+1 -5
View File
@@ -1,5 +1 @@
We track contributions on a per-patch basis using git.
Please see our published git log:
* https://github.com/nightscout/cgm-remote-monitor/commits/master
Copyright (C) 2015 The Nightscout Foundation, http://www.nightscoutfoundation.org
+8 -10
View File
@@ -1,4 +1,4 @@
cgm-remote-monitor (a.k.a. NightScout)
cgm-remote-monitor (a.k.a. Nightscout)
======================================
[![Build Status](https://travis-ci.org/nightscout/cgm-remote-monitor.png)](https://travis-ci.org/nightscout/cgm-remote-monitor)
@@ -7,12 +7,12 @@ cgm-remote-monitor (a.k.a. NightScout)
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
This acts as a web-based CGM (Continuous Glucose Montinor) to allow
multiple caregivers to remotely view a patients glucose data in
realtime. The server reads a MongoDB which is intended to be data
This acts as a web-based CGM (Continuous Glucose Monitor) to allow
multiple caregivers to remotely view a patient's glucose data in
real time. The server reads a MongoDB which is intended to be data
from a physical CGM, where it sends new SGV (sensor glucose values) as
the data becomes available. The data is then displayed graphically
and blood glucose values are predicted 0.5 hours ahead using a
and blood glucose values are predicted 0.5 hours ahead using an
autoregressive second order model. Alarms are generated for high and
low values, which can be cleared by any watcher of the data.
@@ -93,8 +93,8 @@ More questions?
Feel free to [post an issue][issues], but read the [wiki][wiki] first.
[issues]: https://github.com/rnpenguin/cgm-remote-monitor/issues
[wiki]: https://github.com/rnpenguin/cgm-remote-monitor/wiki
[issues]: https://github.com/nightscout/cgm-remote-monitor/issues
[wiki]: https://github.com/nightscout/cgm-remote-monitor/wiki
License
---------------
@@ -102,9 +102,7 @@ License
[agpl-3]: http://www.gnu.org/licenses/agpl-3.0.txt
cgm-remote-monitor - web app to broadcast cgm readings
Copyright (C) 2014 Nightscout contributors. See the COPYRIGHT file
at the root directory of this distribution and at
https://github.com/nightscout/cgm-remote-monitor/blob/master/COPYRIGHT
Copyright (C) 2015 The Nightscout Foundation, http://www.nightscoutfoundation.org.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nightscout",
"version": "0.5.1",
"version": "0.6.0",
"dependencies": {
"angularjs": "1.3.0-beta.19",
"bootstrap": "~3.2.0",
+26
View File
@@ -85,6 +85,32 @@ function config ( ) {
, bg_low: readIntENV('BG_LOW', 55)
};
//NOTE: using +/- 1 here to make the thresholds look visibly wrong in the UI
// if all thresholds were set to the same value you should see 4 lines stacked right on top of each other
if (env.thresholds.bg_target_bottom >= env.thresholds.bg_target_top) {
console.warn('BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ') was >= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')');
env.thresholds.bg_target_bottom = env.thresholds.bg_target_top - 1;
console.warn('BG_TARGET_BOTTOM is now ' + env.thresholds.bg_target_bottom);
}
if (env.thresholds.bg_target_top <= env.thresholds.bg_target_bottom) {
console.warn('BG_TARGET_TOP(' + env.thresholds.bg_target_top + ') was <= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')');
env.thresholds.bg_target_top = env.thresholds.bg_target_bottom + 1;
console.warn('BG_TARGET_TOP is now ' + env.thresholds.bg_target_top);
}
if (env.thresholds.bg_low >= env.thresholds.bg_target_bottom) {
console.warn('BG_LOW(' + env.thresholds.bg_low + ') was >= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')');
env.thresholds.bg_low = env.thresholds.bg_target_bottom - 1;
console.warn('BG_LOW is now ' + env.thresholds.bg_low);
}
if (env.thresholds.bg_high <= env.thresholds.bg_target_top) {
console.warn('BG_HIGH(' + env.thresholds.bg_high + ') was <= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')');
env.thresholds.bg_high = env.thresholds.bg_target_top + 1;
console.warn('BG_HIGH is now ' + env.thresholds.bg_high);
}
//if any of the BG_* thresholds are set, default to "simple" otherwise default to "predict" to preserve current behavior
var thresholdsSet = readIntENV('BG_HIGH') || readIntENV('BG_TARGET_TOP') || readIntENV('BG_TARGET_BOTTOM') || readIntENV('BG_LOW');
env.alarm_types = readENV('ALARM_TYPES') || (thresholdsSet ? "simple" : "predict");
+2
View File
@@ -124,12 +124,14 @@ function update() {
obj.y = element.mbg;
obj.x = element.date;
obj.d = element.dateString;
obj.device = element.device;
mbgData.push(obj);
} else if (element.sgv) {
var obj = {};
obj.y = element.sgv;
obj.x = element.date;
obj.d = element.dateString;
obj.device = element.device;
obj.direction = directionToChar(element.direction);
cgmData.push(obj);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Nightscout",
"version": "0.5.1",
"version": "0.6.0",
"description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.",
"license": "AGPL3",
"author": "Nightscout Team",
+55 -34
View File
@@ -224,37 +224,6 @@ div.tooltip {
padding: 0 0 10px 0;
}
@media (max-height: 480px) {
html body #toolbar {
float: right;
height: auto;
}
#toolbar h1 {
display: none;
}
html body .container {
top: 15px;
padding-bottom: 20px;
}
html body .container .status {
font-size: 70%;
}
html body #bgButton {
font-size: 80%;
}
html body #chartContainer {
top: 130px;
}
html body #chartContainer svg {
height: calc(100vh - 130px);
}
}
/* Large desktop */
@media (min-width: 980px) {
.content {
@@ -270,7 +239,7 @@ div.tooltip {
@media (max-width: 800px) {
#chartContainer {
font-size: 115%;
font-size: 80%;
}
.bgStatus {
@@ -296,6 +265,10 @@ div.tooltip {
font-size: 60% !important;
}
.container .status {
font-size: 70%;
}
#chartContainer {
top: 165px;
font-size: 80%;
@@ -344,6 +317,10 @@ div.tooltip {
margin-left: 2vw;
width: 96vw;
}
.container .status {
font-size: 100% !important;
}
.time {
margin-bottom: 0.1em;
margin-top: -1em;
@@ -356,7 +333,7 @@ div.tooltip {
}
#currentTime {
display: inline;
font-size: 25%;
font-size: 25% !important;
font-weight: bold;
}
.timeOther {
@@ -365,10 +342,54 @@ div.tooltip {
}
#chartContainer {
font-size: 110%;
font-size: 110% !important;
top: 210px;
}
#chartContainer svg {
height: calc(100vh - (210px));
}
}
@media (max-height: 480px) {
#toolbar {
float: right;
height: auto !important;
}
#toolbar h1 {
display: none;
}
.container {
top: 15px;
padding-bottom: 20px;
}
#bgButton {
font-size: 70% !important;
}
#currentTime {
font-size: 85%;
}
#chartContainer {
top: 130px;
font-size: 80%;
}
#chartContainer svg {
height: calc(100vh - 130px);
}
}
@media (max-height: 480px) and (min-width: 400px) {
.container .status {
font-size: 70%;
}
}
@media (max-height: 480px) and (max-width: 400px) {
.container .status {
font-size: 80% !important;
}
}
+14 -10
View File
@@ -4,11 +4,11 @@
<meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=0" />
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/images/logomobile.png">
<title>NightScout</title>
<title>Nightscout</title>
<link href="/images/round1.png" rel="icon" id="favicon" type="image/png" />
<link rel="stylesheet" type="text/css" href="/css/main.css?v=0.5.x-12-13" />
<link rel="stylesheet" type="text/css" href="/css/main.css?v=0.6.0" />
<link rel="stylesheet" type="text/css" href="/css/dropdown.css" />
<link rel="stylesheet" type="text/css" href="/css/drawer.css?v=0.5.x-12-13" />
<link rel="stylesheet" type="text/css" href="/css/drawer.css?v=0.6.0" />
<link rel="stylesheet" type="text/css" href="/bower_components/tipsy-jmalonzo/src/stylesheets/tipsy.css" />
</head>
<body>
@@ -112,13 +112,17 @@
<div class="appName"></div>
<div>version <span class="version"></span></div>
<div>head <span class="head"></span></div>
<p>
<div>License: <a href="http://www.gnu.org/licenses/agpl.html" target="_blank">AGPL</a></div>
<div>Copyright © 2015 <a href="http://www.nightscoutfoundation.org/" target="_blank">The Nightscout Foundation</a></div>
</p>
<div class="links">
<a href="https://github.com/nightscout/cgm-remote-monitor/releases">Release Notes</a><br />
<a href="http://github.com/nightscout/cgm-remote-monitor">Open Source</a><br />
<a href="http://www.nightscout.info">Community</a><br />
<a href="https://github.com/nightscout/cgm-remote-monitor/releases" target="_blank">Release Notes</a><br />
<a href="http://github.com/nightscout/cgm-remote-monitor" target="_blank">Open Source</a><br />
<a href="http://www.nightscout.info" target="_blank">Nightscout Info</a><br />
<a href="http://www.nightscoutfoundation.org/donations/" target="_blank">Help Support Nightscout</a><br />
<a href="https://www.facebook.com/groups/cgminthecloud/" target="_blank">CGM in the Cloud</a><br />
</div>
<div>License: <a href="http://www.gnu.org/licenses/agpl.html">AGPL</a></div>
<div>Copyright © 2014 Nightscout contributors</div>
</fieldset>
</form>
</div>
@@ -215,8 +219,8 @@
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script src="/bower_components/jQuery-Storage-API/jquery.storageapi.min.js"></script>
<script src="/bower_components/tipsy-jmalonzo/src/javascripts/jquery.tipsy.js"></script>
<script src="/js/ui-utils.js?v=0.5.x-12-13"></script>
<script src="/js/client.js?v=0.5.x-12-13"></script>
<script src="/js/ui-utils.js?v=0.6.0"></script>
<script src="/js/client.js?v=0.6.0"></script>
<script src="/js/experiments.js"></script>
</body>
</html>
+220 -151
View File
@@ -2,12 +2,14 @@
var app = {}, browserSettings = {}, browserStorage = $.localStorage;
(function () {
"use strict";
'use strict';
var BRUSH_TIMEOUT = 300000 // 5 minutes in ms
, TOOLTIP_TRANS_MS = 200 // milliseconds
, UPDATE_TRANS_MS = 750 // milliseconds
, ONE_MIN_IN_MS = 60000
, FIVE_MINS_IN_MS = 300000
, SIX_MINS_IN_MS = 360000
, TWENTY_FIVE_MINS_IN_MS = 1500000
, THIRTY_MINS_IN_MS = 1800000
, SIXTY_MINS_IN_MS = 3600000
@@ -16,6 +18,8 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
, FORMAT_TIME_24 = '%H:%M%'
, FORMAT_TIME_SCALE = '%I %p'
, WIDTH_TIME_HIDDEN = 500
, WIDTH_SMALL_DOTS = WIDTH_TIME_HIDDEN
, WIDTH_BIG_DOTS = 800
, MINUTES_SINCE_LAST_UPDATE_WARN = 10
, MINUTES_SINCE_LAST_UPDATE_URGENT = 20;
@@ -35,7 +39,8 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
, alarmSound = 'alarm.mp3'
, urgentAlarmSound = 'alarm2.mp3';
var div
var jqWindow
, tooltip
, tickValues
, charts
, futureOpacity
@@ -65,7 +70,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
function getTimeFormat(isForScale) {
var timeFormat = FORMAT_TIME_12;
if (browserSettings.timeFormat) {
if (browserSettings.timeFormat == "24") {
if (browserSettings.timeFormat == '24') {
timeFormat = FORMAT_TIME_24;
}
}
@@ -78,20 +83,20 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
}
var x2TickFormat = d3.time.format.multi([
[".%L", function(d) { return d.getMilliseconds(); }],
[":%S", function(d) { return d.getSeconds(); }],
["%I:%M", function(d) { return d.getMinutes(); }],
[(getTimeFormat() == FORMAT_TIME_12) ? "%I %p": '%H:%M%', function(d) { return d.getHours(); }],
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
["%b %d", function(d) { return d.getDate() != 1; }],
["%B", function(d) { return d.getMonth(); }],
["%Y", function() { return true; }]
['.%L', function(d) { return d.getMilliseconds(); }],
[':%S', function(d) { return d.getSeconds(); }],
['%I:%M', function(d) { return d.getMinutes(); }],
[(getTimeFormat() == FORMAT_TIME_12) ? '%I %p': '%H:%M%', function(d) { return d.getHours(); }],
['%a %d', function(d) { return d.getDay() && d.getDate() != 1; }],
['%b %d', function(d) { return d.getDate() != 1; }],
['%B', function(d) { return d.getMonth(); }],
['%Y', function() { return true; }]
]);
// lixgbg: Convert mg/dL BG value to metric mmol
function scaleBg(bg) {
if (browserSettings.units == "mmol") {
if (browserSettings.units == 'mmol') {
return (Math.round((bg / 18) * 10) / 10).toFixed(1);
} else {
return bg;
@@ -218,10 +223,10 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
var focusData = data.slice();
if (alarmInProgress) {
if ($(window).width() > WIDTH_TIME_HIDDEN) {
$(".time").show();
if (jqWindow.width() > WIDTH_TIME_HIDDEN) {
$('.time').show();
} else {
$(".time").hide();
$('.time').hide();
}
}
@@ -243,7 +248,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
d.type == 'sgv';
});
// sometimes nowDataRaw contains duplicates. uniq it.
var lastDate = new Date("1/1/1970");
var lastDate = new Date('1/1/1970');
var nowData = nowDataRaw.filter(function(n) {
if ( (lastDate.getTime() + ONE_MIN_IN_MS) < n.date.getTime()) {
lastDate = n.date;
@@ -264,20 +269,20 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
else
$('.container .currentBG').text(focusPoint.sgv);
var retroDelta = scaleBg(focusPoint.y) - scaleBg(prevfocusPoint.y);
if (browserSettings.units == "mmol") {
if (browserSettings.units == 'mmol') {
retroDelta = retroDelta.toFixed(1);
}
if (retroDelta < 0) {
var retroDeltaString = retroDelta;
}
else {
var retroDeltaString = "+" + retroDelta;
var retroDeltaString = '+' + retroDelta;
}
if (browserSettings.units == "mmol") {
var retroDeltaString = retroDeltaString + " mmol/L"
if (browserSettings.units == 'mmol') {
var retroDeltaString = retroDeltaString + ' mmol/L'
}
else {
var retroDeltaString = retroDeltaString + " mg/dL"
var retroDeltaString = retroDeltaString + ' mg/dL'
}
$('.container .currentBG').css('text-decoration','line-through');
@@ -287,7 +292,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
$('.container .currentDirection').html(focusPoint.direction)
} else {
$('.container .currentBG')
.text("---")
.text('---')
.css('text-decoration','');
$('.container .currentDelta').text('');
}
@@ -295,7 +300,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.text(formatTime(new Date(brushExtent[1] - THIRTY_MINS_IN_MS)))
.css('text-decoration','line-through');
$('#lastEntry').text("RETRO").removeClass('current');
$('#lastEntry').text('RETRO').removeClass('current');
$('.container #noButton .currentBG').css({color: 'grey'});
$('.container #noButton .currentDelta').css({color: 'grey'});
@@ -335,7 +340,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
default: errorDisplay = '?' + parseInt(errorCode) + '?'; break;
}
$('#lastEntry').text("CGM ERROR").removeClass('current').addClass("urgent");
$('#lastEntry').text('CGM ERROR').removeClass('current').addClass('urgent');
$('.container .currentBG').html(errorDisplay)
.css('text-decoration', '');
@@ -353,28 +358,29 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
$('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60);
//in this case the SGV is unscaled
if (latestSGV.y < 40)
if (latestSGV.y < 40) {
$('.container .currentBG').text('LOW');
else if (latestSGV.y > 400)
} else if (latestSGV.y > 400) {
$('.container .currentBG').text('HIGH');
else
} else {
$('.container .currentBG').text(scaleBg(latestSGV.y));
var bgDelta = scaleBg(latestSGV.y) - scaleBg(prevSGV.y);
if (browserSettings.units == "mmol") {
bgDelta = bgDelta.toFixed(1);
}
if (bgDelta < 0) {
var bgDeltaString = bgDelta;
}
else {
var bgDeltaString = "+" + bgDelta;
}
if (browserSettings.units == "mmol") {
var bgDeltaString = bgDeltaString + " mmol/L"
}
else {
var bgDeltaString = bgDeltaString + " mg/dL"
}
}
var bgDelta = scaleBg(latestSGV.y) - scaleBg(prevSGV.y);
if (browserSettings.units == 'mmol') {
bgDelta = bgDelta.toFixed(1);
}
var bgDeltaString = bgDelta;
if (bgDelta >= 0) {
bgDeltaString = '+' + bgDelta;
}
if (browserSettings.units == 'mmol') {
bgDeltaString = bgDeltaString + ' mmol/L'
} else {
bgDeltaString = bgDeltaString + ' mg/dL'
}
$('.container .currentBG').css('text-decoration', '');
$('.container .currentDelta')
@@ -402,6 +408,12 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
// selects all our data into data and uses date function to get current max date
var focusCircles = focus.selectAll('circle').data(focusData, dateFn);
var dotRadius = function(type) {
var radius = prevChartWidth > WIDTH_BIG_DOTS ? 4 : (prevChartWidth < WIDTH_SMALL_DOTS ? 2 : 3);
if (type == 'mbg') radius *= 2;
return radius;
};
// if already existing then transition each circle to its new position
focusCircles
.transition()
@@ -409,6 +421,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.attr('cx', function (d) { return xScale(d.date); })
.attr('cy', function (d) { return yScale(d.sgv); })
.attr('fill', function (d) { return d.color; })
.attr('r', function (d) {return dotRadius(d.type); })
.attr('opacity', function (d) { return futureOpacity(d.date.getTime() - latestSGV.x); });
// if new circle then just display
@@ -418,8 +431,30 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.attr('fill', function (d) { return d.color; })
.attr('opacity', function (d) { return futureOpacity(d.date.getTime() - latestSGV.x); })
.attr('stroke-width', function (d) {if (d.type == 'mbg') return 2; else return 0; })
.attr('stroke', function (d) { return "white"; })
.attr('r', function(d) { if (d.type == 'mbg') return 6; else return 3;});
.attr('stroke', function (d) {
var device = d.device && d.device.toLowerCase();
return (device == 'dexcom' ? 'white' : '#0099ff');
})
.attr('r', function (d) {return dotRadius(d.type); })
.on('mouseover', function (d) {
if (d.type != 'sgv' && d.type != 'mbg') return;
var device = d.device && d.device.toLowerCase();
var bgType = (d.type == 'sgv' ? 'CGM' : (device == 'dexcom' ? 'Calibration' : 'Meter'));
tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
tooltip.html('<strong>' + bgType + ' BG:</strong> ' + d.sgv +
(d.type == 'mbg' ? '<br/><strong>Device: </strong>' + d.device : '') +
'<br/><strong>Time:</strong> ' + formatTime(d.date))
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY - 28) + 'px');
})
.on('mouseout', function (d) {
if (d.type != 'sgv' && d.type != 'mbg') return;
tooltip.transition()
.duration(TOOLTIP_TRANS_MS)
.style('opacity', 0);
});
focusCircles.exit()
.remove();
@@ -428,11 +463,11 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
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) });
// a higher bubbleScale will produce smaller bubbles (it's not a radius like focusDotRadius)
var bubbleScale = prevChartWidth < WIDTH_SMALL_DOTS ? 4 : (prevChartWidth < WIDTH_BIG_DOTS ? 3 : 2);
focus.selectAll('circle')
.data(treatments)
.each(function (d) { drawTreatment(d, bubbleScale, true) });
// transition open-top line to correct location
focus.select('.open-top')
@@ -463,6 +498,13 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.attr('x2', xScale(nowDate))
.attr('y2', yScale(scaleBg(420)));
context.select('.now-line')
.transition()
.attr('x1', xScale2(new Date(brush.extent()[1]- THIRTY_MINS_IN_MS)))
.attr('y1', yScale2(scaleBg(36)))
.attr('x2', xScale2(new Date(brush.extent()[1]- THIRTY_MINS_IN_MS)))
.attr('y2', yScale2(scaleBg(420)));
// update x axis
focus.select('.x.axis')
.call(xAxis);
@@ -471,51 +513,45 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
focusCircles.attr('clip-path', 'url(#clip)');
try {
//NOTE: treatments with insulin or carbs are drawn by drawTreatment()
//TODO: integrate with drawTreatment()
// bind up the focus chart data to an array of circles
var treatCircles = focus.selectAll('rect').data(treatments);
var treatCircles = focus.selectAll('rect').data(treatments.filter(function(treatment) {
return !treatment.carbs && !treatment.insulin;
}));
// if already existing then transition each circle to its new position
treatCircles.transition()
.duration(UPDATE_TRANS_MS)
.attr('x', function (d) { return xScale(new Date(d.created_at)); })
.attr('y', function (d) { return yScale(scaleBg(500)); })
.attr("width", 15)
.attr("height", 15)
.attr("rx", 6)
.attr("ry", 6)
.attr('stroke-width', 2)
.attr('stroke', function (d) { return "white"; })
.attr('fill', function (d) { return "grey"; });
.attr('cx', function (d) { return xScale(new Date(d.created_at)); })
.attr('cy', function (d) { return yScale(scaleBg(d.glucose || calcBGByTime(d.created_at.getTime()))); });
// if new circle then just display
treatCircles.enter().append('rect')
.attr('x', function (d) { return xScale(d.created_at); })
.attr('y', function (d) { return yScale(scaleBg(500)); })
.attr("width", 15)
.attr("height", 15)
.attr("rx", 6)
.attr("ry", 6)
treatCircles.enter().append('circle')
.attr('cx', function (d) { return xScale(d.created_at); })
.attr('cy', function (d) { return yScale(scaleBg(d.glucose || calcBGByTime(d.created_at.getTime()))); })
.attr('r', function () { return dotRadius('mbg'); })
.attr('stroke-width', 2)
.attr('stroke', function (d) { return "white"; })
.attr('fill', function (d) { return "grey"; })
.on("mouseover", function (d) {
div.transition().duration(200).style("opacity", .9);
div.html("<strong>Time:</strong> " + formatTime(d.created_at) + "<br/>" + "<strong>Treatment type:</strong> " + d.eventType + "<br/>" +
(d.carbs ? "<strong>Carbs:</strong> " + d.carbs + "<br/>" : '') +
(d.insulin ? "<strong>Insulin:</strong> " + d.insulin + "<br/>" : '') +
(d.glucose ? "<strong>BG:</strong> " + d.glucose + (d.glucoseType ? ' (' + d.glucoseType + ')': '') + "<br/>" : '') +
(d.enteredBy ? "<strong>Entered by:</strong> " + d.enteredBy + "<br/>" : '') +
(d.notes ? "<strong>Notes:</strong> " + d.notes : '')
.attr('stroke', function (d) { return d.glucose ? 'grey' : 'white'; })
.attr('fill', function (d) { return d.glucose ? 'red' : 'grey'; })
.on('mouseover', function (d) {
tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
tooltip.html('<strong>Time:</strong> ' + formatTime(d.created_at) + '<br/>' +
(d.eventType ? '<strong>Treatment type:</strong> ' + d.eventType + '<br/>' : '') +
(d.glucose ? '<strong>BG:</strong> ' + d.glucose + (d.glucoseType ? ' (' + d.glucoseType + ')': '') + '<br/>' : '') +
(d.enteredBy ? '<strong>Entered by:</strong> ' + d.enteredBy + '<br/>' : '') +
(d.notes ? '<strong>Notes:</strong> ' + d.notes : '')
)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY - 28) + 'px');
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
.on('mouseout', function () {
tooltip.transition()
.duration(TOOLTIP_TRANS_MS)
.style('opacity', 0);
});
treatCircles.attr('clip-path', 'url(#clip)');
} catch (err) {
@@ -810,14 +846,6 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
// 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);
@@ -850,7 +878,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.attr('fill', function (d) { return d.color; })
.style('opacity', function (d) { return highlightBrushPoints(d) })
.attr('stroke-width', function (d) {if (d.type == 'mbg') return 2; else return 0; })
.attr('stroke', function (d) { return "white"; })
.attr('stroke', function (d) { return 'white'; })
.attr('r', function(d) { if (d.type == 'mbg') return 4; else return 2;});
contextCircles.exit()
@@ -864,7 +892,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
function sgvToColor(sgv) {
var color = 'grey';
if (browserSettings.theme == "colors") {
if (browserSettings.theme == 'colors') {
if (sgv > app.thresholds.bg_high) {
color = 'red';
} else if (sgv > app.thresholds.bg_target_top) {
@@ -891,22 +919,22 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
});
var bgButton = $('#bgButton');
bgButton.show();
bgButton.toggleClass("urgent", file == urgentAlarmSound);
bgButton.toggleClass('urgent', file == urgentAlarmSound);
var noButton = $('#noButton');
noButton.hide();
$('.container .currentBG').text();
if ($(window).width() <= WIDTH_TIME_HIDDEN) {
$(".time").hide();
if (jqWindow.width() <= WIDTH_TIME_HIDDEN) {
$('.time').hide();
}
}
function playAlarm(audio) {
// ?mute=true disables alarms to testers.
if (querystring.mute != "true") {
if (querystring.mute != 'true') {
audio.play();
} else {
showNotification("Alarm is muted per your request. (?mute=true)");
showNotification('Alarm is muted per your request. (?mute=true)');
}
}
@@ -922,7 +950,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
$(this).removeClass('playing');
});
$(".time").show();
$('.time').show();
// only emit ack if client invoke by button press
if (isClient) {
@@ -952,18 +980,18 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
else parts = { label: 'a long time ago' };
if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_URGENT)) {
var lastEntry = $("#lastEntry");
lastEntry.removeClass("warn");
lastEntry.addClass("urgent");
var lastEntry = $('#lastEntry');
lastEntry.removeClass('warn');
lastEntry.addClass('urgent');
$(".bgStatus").removeClass("current");
$('.bgStatus').removeClass('current');
} else if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_WARN)) {
var lastEntry = $("#lastEntry");
lastEntry.removeClass("urgent");
lastEntry.addClass("warn");
var lastEntry = $('#lastEntry');
lastEntry.removeClass('urgent');
lastEntry.addClass('warn');
} else {
$(".bgStatus").addClass("current");
$("#lastEntry").removeClass("warn urgent");
$('.bgStatus').addClass('current');
$('#lastEntry').removeClass('warn urgent');
}
if (parts.value)
@@ -973,31 +1001,50 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
}
function calcBGByTime(time) {
var closeBGs = data.filter(function(d) {
if (!d.y) {
return false;
} else {
return Math.abs((new Date(d.date)).getTime() - time) <= SIX_MINS_IN_MS;
}
});
var totalBG = 0;
closeBGs.forEach(function(d) {
totalBG += d.y;
});
return totalBG ? (totalBG / closeBGs.length) : 450;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//draw a compact visualization of a treatment (carbs, insulin)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function drawTreatment(treatment, scale, showValues) {
var carbs = treatment.carbs;
var insulin = treatment.insulin;
var CR = treatment.CR;
if (!treatment.carbs && !treatment.insulin) return;
var CR = treatment.CR || 20;
var carbs = treatment.carbs || CR;
var insulin = treatment.insulin || 1;
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': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 },
{ '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': '#0099ff', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 },
{ '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';
arc_data[0].outlineOnly = !treatment.carbs;
arc_data[2].outlineOnly = !treatment.insulin;
if (treatment.carbs > 0) arc_data[1].element = Math.round(treatment.carbs) + ' g';
if (treatment.insulin > 0) arc_data[3].element = Math.round(treatment.insulin * 100) / 100 + ' U';
var arc = d3.svg.arc()
.innerRadius(function (d) { return 5 * d.inner; })
@@ -1009,11 +1056,29 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
.data(arc_data)
.enter()
.append('g')
.attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')');
.attr('transform', 'translate(' + xScale(treatment.created_at.getTime()) + ', ' + yScale(scaleBg(treatment.glucose || calcBGByTime(treatment.created_at.getTime()))) + ')')
.on('mouseover', function () {
tooltip.transition().duration(200).style('opacity', .9);
tooltip.html('<strong>Time:</strong> ' + formatTime(treatment.created_at) + '<br/>' + '<strong>Treatment type:</strong> ' + treatment.eventType + '<br/>' +
(treatment.carbs ? '<strong>Carbs:</strong> ' + treatment.carbs + '<br/>' : '') +
(treatment.insulin ? '<strong>Insulin:</strong> ' + treatment.insulin + '<br/>' : '') +
(treatment.glucose ? '<strong>BG:</strong> ' + treatment.glucose + (treatment.glucoseType ? ' (' + treatment.glucoseType + ')': '') + '<br/>' : '') +
(treatment.enteredBy ? '<strong>Entered by:</strong> ' + treatment.enteredBy + '<br/>' : '') +
(treatment.notes ? '<strong>Notes:</strong> ' + treatment.notes : '')
)
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY - 28) + 'px');
})
.on('mouseout', function () {
tooltip.transition()
.duration(500)
.style('opacity', 0);
});
var arcs = treatmentDots.append('path')
.attr('class', 'path')
.attr('fill', function (d, i) { return d.color; })
.attr('fill', function (d, i) { if (d.outlineOnly) return 'transparent'; else return d.color; })
.attr('stroke-width', function (d) {if (d.outlineOnly) return 1; else return 0; })
.attr('stroke', function (d) { return d.color; })
.attr('id', function (d, i) { return 's' + i; })
.attr('d', arc);
@@ -1027,6 +1092,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
label.append('text')
.style('font-size', 30 / scale)
.style('font-family', 'Arial')
.style('text-shadow', '0px 0px 10px rgba(0, 0, 0, 1)')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('transform', function (d) {
@@ -1034,7 +1100,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
d.innerRadius = d.outerRadius * 2.1;
return 'translate(' + arc.centroid(d) + ')';
})
.text(function (d) { return d.element; })
.text(function (d) { return d.element; });
}
}
@@ -1058,7 +1124,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
var y = [Math.log(actual[actual.length-1].sgv / BG_REF), Math.log(actual[actual.length-1].sgv / BG_REF)];
} else {
var elapsedMins = (actual[actual.length-1].date - actual[actual.length-1-lookback].date) / ONE_MINUTE;
// construct a "5m ago" sgv offset from current sgv by the average change over the lookback interval
// construct a '5m ago' sgv offset from current sgv by the average change over the lookback interval
var lookbackSgvChange = actual[lookback].sgv-actual[0].sgv;
var fiveMinAgoSgv = actual[lookback].sgv - lookbackSgvChange/elapsedMins*5;
y = [Math.log(fiveMinAgoSgv / BG_REF), Math.log(actual[lookback].sgv / BG_REF)];
@@ -1073,7 +1139,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
var AR = [-0.723, 1.716];
var dt = actual[lookback].date.getTime();
var predictedColor = 'blue';
if (browserSettings.theme == "colors") {
if (browserSettings.theme == 'colors') {
predictedColor = 'cyan';
}
for (var i = 0; i < CONE.length; i++) {
@@ -1094,19 +1160,22 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
predicted.forEach(function (d) {
d.type = 'forecast';
if (d.sgv < BG_MIN)
d.color = "transparent";
d.color = 'transparent';
})
}
return predicted;
}
function init() {
div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
jqWindow = $(window);
tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Tick Values
if (browserSettings.units == "mmol") {
if (browserSettings.units == 'mmol') {
tickValues = [
2.0
, Math.round(scaleBg(app.thresholds.bg_low))
@@ -1167,14 +1236,14 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
}, 100);
};
var silenceDropdown = new Dropdown(".dropdown-menu");
var silenceDropdown = new Dropdown('.dropdown-menu');
$('#bgButton').click(function (e) {
silenceDropdown.open(e);
});
$("#silenceBtn").find("a").click(function (e) {
stopAlarm(true, $(this).data("snooze-time"));
$('#silenceBtn').find('a').click(function (e) {
stopAlarm(true, $(this).data('snooze-time'));
e.preventDefault();
});
@@ -1213,16 +1282,16 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
// 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
// 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), y: obj.y, sgv: scaleBg(obj.y), color: 'none', type: 'server-forecast'} }));
//Add MBG's also, pretend they are SGV's
data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), y: obj.y, sgv: scaleBg(obj.y), color: 'red', type: 'mbg'} }));
data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), y: obj.y, sgv: scaleBg(obj.y), color: 'red', type: 'mbg', device: obj.device } }));
data.forEach(function (d) {
if (d.y < 39)
d.color = "transparent";
d.color = 'transparent';
});
treatments = d[3];
@@ -1260,27 +1329,27 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
}
socket.on('alarm', function () {
console.info("alarm received from server");
console.info('alarm received from server');
var enabled = (isAlarmForHigh() && browserSettings.alarmHigh) || (isAlarmForLow() && browserSettings.alarmLow);
if (enabled) {
console.log("Alarm raised!");
console.log('Alarm raised!');
currentAlarmType = 'alarm';
generateAlarm(alarmSound);
} else {
console.info("alarm was disabled locally", latestSGV.y, browserSettings);
console.info('alarm was disabled locally', latestSGV.y, browserSettings);
}
brushInProgress = false;
updateChart(false);
});
socket.on('urgent_alarm', function () {
console.info("urgent alarm received from server");
console.info('urgent alarm received from server');
var enabled = (isAlarmForHigh() && browserSettings.alarmUrgentHigh) || (isAlarmForLow() && browserSettings.alarmUrgentLow);
if (enabled) {
console.log("Urgent alarm raised!");
console.log('Urgent alarm raised!');
currentAlarmType = 'urgent_alarm';
generateAlarm(urgentAlarmSound);
} else {
console.info("urgent alarm was disabled locally", latestSGV.y, browserSettings);
console.info('urgent alarm was disabled locally', latestSGV.y, browserSettings);
}
brushInProgress = false;
updateChart(false);
@@ -1305,7 +1374,7 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
});
}
$.ajax("/api/v1/status.json", {
$.ajax('/api/v1/status.json', {
success: function (xhr) {
app = { name: xhr.name
, version: xhr.version
@@ -1317,13 +1386,13 @@ var app = {}, browserSettings = {}, browserStorage = $.localStorage;
};
}
}).done(function() {
$(".appName").text(app.name);
$(".version").text(app.version);
$(".head").text(app.head);
$('.appName').text(app.name);
$('.version').text(app.version);
$('.head').text(app.head);
if (app.apiEnabled) {
$(".serverSettings").show();
$('.serverSettings').show();
}
$("#treatmentDrawerToggle").toggle(app.careportalEnabled);
$('#treatmentDrawerToggle').toggle(app.careportalEnabled);
browserSettings = getBrowserSettings(browserStorage);
init();
});