Skip to content

Commit

Permalink
Merge pull request #18 from nightscout/retrospective-prediction
Browse files Browse the repository at this point in the history
Adding retrospective predictor. When viewing historical data, time and s...
  • Loading branch information
bewest committed Jun 22, 2014
2 parents f46f3a3 + 15882e4 commit 75d1c52
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 12 deletions.
4 changes: 1 addition & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@
<span class="currentDirection">-</span>
</div>
<ul class="dropdown-menu" id="silenceBtn">
<li><a href="#" data-snooze-time="1200000">Silence for 20 minutes</a></li>
<li><a href="#" data-snooze-time="1800000">Silence for 30 minutes</a></li>
<li><a href="#" data-snooze-time="2400000">Silence for 40 minutes</a></li>
<li><a href="#" data-snooze-time="3000000">Silence for 50 minutes</a></li>
<li><a href="#" data-snooze-time="3600000">Silence for 60 minutes</a></li>
<li><a href="#" data-snooze-time="5400000">Silence for 90 minutes</a></li>
</ul>
</div>
</div>
Expand Down
110 changes: 101 additions & 9 deletions js/client.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
(function() {
"use strict";

var treatments,
var retrospectivePredictor = true,
latestSGV,
treatments,
padding = {top: 20, right: 10, bottom: 80, left: 10},
opacity = {current: 1, DAY: 1, NIGHT: 0.8},
now = Date.now(),
Expand All @@ -19,6 +21,9 @@
brushTimer,
brushInProgress = false,
clip,
TWENTY_FIVE_MINS_IN_MS = 1500000,
THIRTY_MINS_IN_MS = 1800000,
FORTY_TWO_MINS_IN_MS = 2520000,
FOCUS_DATA_RANGE_MS = 12600000, // 3.5 hours of actual data
audio = document.getElementById('audio'),
alarmInProgress = false,
Expand Down Expand Up @@ -158,22 +163,67 @@

// 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([new Date(brushExtent[1].getTime() - FOCUS_DATA_RANGE_MS), brushExtent[1]]));
.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], new Date(brushExtent[0].getTime() + FOCUS_DATA_RANGE_MS)]));
.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();

var element = document.getElementById('bgButton').hidden == '';

// predict for retrospective data
if (retrospectivePredictor && 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
});
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','none');
}
$('#currentTime')
.text(d3.time.format('%I:%M%p')(brushExtent[1]))
.css('text-decoration','line-through');
} else if (retrospectivePredictor) {
// if the brush comes back into the current time range then it should reset to the current time and sg
var dateTime = new Date(now);
$('#currentTime')
.text(d3.time.format('%I:%M%p')(dateTime))
.css('text-decoration','none');
$('.container .currentBG')
.text(latestSGV.y)
.css('text-decoration','none');
$('.container .currentDirection')
.html(latestSGV.direction);
}

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(data, dateFn);
var focusCircles = focus.selectAll('circle').data(focusData, dateFn);

// if already existing then transition each circle to its new position
focusCircles.transition()
focusCircles
.transition()
.duration(UPDATE_TRANS_MS)
.attr('cx', function (d) { return xScale(d.date); })
.attr('cy', function (d) { return yScale(d.sgv); })
Expand Down Expand Up @@ -222,9 +272,9 @@
focus.select('.now-line')
.transition()
.duration(UPDATE_TRANS_MS)
.attr('x1', xScale(new Date(now)))
.attr('x1', xScale(new Date(brushExtent[1].getTime() - THIRTY_MINS_IN_MS)))
.attr('y1', yScale(36))
.attr('x2', xScale(new Date(now)))
.attr('x2', xScale(new Date(brushExtent[1].getTime() - THIRTY_MINS_IN_MS)))
.attr('y2', yScale(420));

// update x axis
Expand Down Expand Up @@ -581,6 +631,7 @@
// change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr)
if (d[0].length) {
var current = d[0][d[0].length - 1];
latestSGV = current;
var secsSinceLast = (Date.now() - new Date(current.x).getTime()) / 1000;
var currentBG = current.y;

Expand All @@ -605,7 +656,7 @@
$('.container .currentDirection').html(current.direction);
$('.container .current').toggleClass('high', current.y > 180).toggleClass('low', current.y < 70)
}
data = d[0].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, color: 'grey'} });
data = d[0].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, direction: obj.direction, color: 'grey'} });
data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, color: 'blue'} }));
data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, color: 'red'} }));
treatments = d[3];
Expand All @@ -630,18 +681,23 @@
console.log("Alarm raised!");
currentAlarmType = 'alarm';
generateAlarm(alarmSound);
brushInProgress = false;
updateChart(false);
});
socket.on('urgent_alarm', function() {
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();
}
});
// TODO: this is dead code, maybe delete and remove from server
socket.on('clients', function(watchers) {
console.log('number of clients has changed to ' + watchers);
$('#watchers').text(watchers);
Expand Down Expand Up @@ -680,6 +736,7 @@
// only emit ack if client invoke by button press
if (isClient) {
socket.emit('ack', currentAlarmType || 'alarm', silenceTime);
brushed(false);
}
}

Expand Down Expand Up @@ -707,7 +764,7 @@

}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//draw a compact visualization of a treatment (carbs, insulin)
Expand Down Expand Up @@ -773,4 +830,39 @@
.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;
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 n = 20;
var AR = [-0.723, 1.716];
var dt = actual[1].date.getTime();
for (var i = 0; i <= n; i++) {
y = [y[1], AR[0] * y[0] + AR[1] * y[1]];
dt = dt + FIVE_MINUTES;
predicted[i] = {
date: new Date(dt+3000),
sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp(y[1])))),
color: 'blue'
};
}
return predicted;
}
})();

0 comments on commit 75d1c52

Please sign in to comment.