Skip to content

Commit

Permalink
Merge pull request #2926 from nightscout/rendering
Browse files Browse the repository at this point in the history
Make SMBs and predictions fit better
  • Loading branch information
sulkaharo committed Nov 1, 2017
2 parents 5f57778 + 6f83031 commit 9e0cc8b
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -23,4 +23,4 @@ coverage/
npm-debug.log
*.heapsnapshot

/tmp
/tmp
2 changes: 1 addition & 1 deletion bower.json
@@ -1,6 +1,6 @@
{
"name": "nightscout",
"version": "0.10.2-release-20171026",
"version": "0.10.2-release-20171027",
"dependencies": {
"colorbrewer": "~1.0.0",
"jQuery-Storage-API": "~1.7.2",
Expand Down
12 changes: 9 additions & 3 deletions lib/client/chart.js
Expand Up @@ -73,9 +73,15 @@ function init (client, d3, $) {
var contextYDomain = [utils.scaleMgdl(36), utils.scaleMgdl(420)];

function dynamicDomain() {
var mult = 1.3
// allow y-axis to extend all the way to the top of the basal area, but leave room to display highest value
var mult = 1.15
, targetTop = client.settings.thresholds.bgTargetTop
, mgdlMax = d3.max(client.entries, function (d) { return d.mgdl; });
// filter to only use actual SGV's (not rawbg's) to set the view window.
// can switch to Logarithmic (non-dynamic) to see anything that doesn't fit in the dynamicDomain
, mgdlMax = d3.max(client.entries, function (d) { if ( d.type === 'sgv') { return d.mgdl; } });
// use the 99th percentile instead of max to avoid rescaling for 1 flukey data point
// need to sort client.entries by mgdl first
//, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; });

return [
utils.scaleMgdl(30)
Expand Down Expand Up @@ -505,7 +511,7 @@ function init (client, d3, $) {

var updateBrush = d3.select('.brush').transition();
updateBrush
.call(chart.brush.extent([new Date(dataRange[1].getTime() - client.foucusRangeMS), dataRange[1]]));
.call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]]));
client.brushed(true);

renderer.addContextCircles();
Expand Down
14 changes: 7 additions & 7 deletions lib/client/index.js
Expand Up @@ -221,7 +221,7 @@ client.load = function load(serverSettings, callback) {

client.hashauth.initAuthentication(client.afterAuth);

client.foucusRangeMS = times.hours(client.settings.focusHours).msecs;
client.focusRangeMS = times.hours(client.settings.focusHours).msecs;
$('.focus-range li[data-hours=' + client.settings.focusHours + ']').addClass('selected');
client.brushed = brushed;
client.formatTime = formatTime;
Expand Down Expand Up @@ -365,7 +365,7 @@ client.load = function load(serverSettings, callback) {
d3.select('.brush')
.transition()
.duration(UPDATE_TRANS_MS)
.call(chart.brush.extent([new Date(dataRange[1].getTime() - client.foucusRangeMS), dataRange[1]]));
.call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]]));

if (!skipBrushing) {
brushed();
Expand All @@ -385,14 +385,14 @@ client.load = function load(serverSettings, callback) {
var brushExtent = chart.brush.extent();

// ensure that brush extent is fixed at 3.5 hours
if (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.foucusRangeMS) {
if (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS) {
// ensure that brush updating is with the time range
if (brushExtent[0].getTime() + client.foucusRangeMS > client.dataExtent()[1].getTime()) {
brushExtent[0] = new Date(brushExtent[1].getTime() - client.foucusRangeMS);
if (brushExtent[0].getTime() + client.focusRangeMS > client.dataExtent()[1].getTime()) {
brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS);
d3.select('.brush')
.call(chart.brush.extent([brushExtent[0], brushExtent[1]]));
} else {
brushExtent[1] = new Date(brushExtent[0].getTime() + client.foucusRangeMS);
brushExtent[1] = new Date(brushExtent[0].getTime() + client.focusRangeMS);
d3.select('.brush')
.call(chart.brush.extent([brushExtent[0], brushExtent[1]]));
}
Expand Down Expand Up @@ -868,7 +868,7 @@ client.load = function load(serverSettings, callback) {
$('.focus-range li').removeClass('selected');
li.addClass('selected');
var hours = Number(li.data('hours'));
client.foucusRangeMS = times.hours(hours).msecs;
client.focusRangeMS = times.hours(hours).msecs;
Storages.localStorage.set('focusHours', hours);
refreshChart();
} else {
Expand Down
63 changes: 43 additions & 20 deletions lib/client/renderer.js
Expand Up @@ -6,7 +6,7 @@ var times = require('../times');
var DEFAULT_FOCUS = times.hours(3).msecs
, WIDTH_SMALL_DOTS = 420
, WIDTH_BIG_DOTS = 800
, TOOLTIP_TRANS_MS = 200 // milliseconds
, TOOLTIP_TRANS_MS = 100 // milliseconds
, TOOLTIP_WIDTH = 150 //min-width + padding
;

Expand All @@ -23,7 +23,7 @@ function init (client, d3) {
}

function focusRangeAdjustment ( ) {
return client.foucusRangeMS === DEFAULT_FOCUS ? 1 : 1 + ((client.foucusRangeMS - DEFAULT_FOCUS) / DEFAULT_FOCUS / 8);
return client.focusRangeMS === DEFAULT_FOCUS ? 1 : 1 + ((client.focusRangeMS - DEFAULT_FOCUS) / DEFAULT_FOCUS / 8);
}

var dotRadius = function(type) {
Expand Down Expand Up @@ -74,6 +74,9 @@ function init (client, d3) {
return client.settings.showForecast.indexOf(point.info.type) > -1;
});
var maxForecastMills = _.max(_.map(shownForecastPoints, function (point) {return point.mills}));
// limit lookahead to the same as lookback
var focusHoursAheadMills = chart().brush.extent()[1].getTime() + client.focusRangeMS;
maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills);
client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0;
focusData = focusData.concat(shownForecastPoints);
}
Expand Down Expand Up @@ -111,7 +114,7 @@ function init (client, d3) {
return d.noFade ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills);
})
.attr('stroke-width', function (d) {
return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 1 : 0;
return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0;
})
.attr('stroke', function (d) {
return (d.type === 'mbg' ? 'white' : d.color);
Expand All @@ -128,7 +131,7 @@ function init (client, d3) {
}

function focusCircleTooltip (d) {
if (d.type !== 'sgv' && d.type !== 'mbg') {
if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') {
return;
}

Expand All @@ -148,6 +151,7 @@ function init (client, d3) {
client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
client.tooltip.html('<strong>' + translate('BG')+ ':</strong> ' + client.sbx.scaleEntry( d ) +
(d.type === 'mbg' ? '<br/><strong>' + translate('Device') + ': </strong>' + d.device : '') +
(d.type === 'forecast' ? '<br/><strong>' + translate('Forecast Type') + ': </strong>' + d.forecastType : '') +
(rawbgInfo.value ? '<br/><strong>' + translate('Raw BG') + ':</strong> ' + rawbgInfo.value : '') +
(rawbgInfo.noise ? '<br/><strong>' + translate('Noise') + ':</strong> ' + rawbgInfo.noise : '') +
'<br/><strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(d.mills)))
Expand Down Expand Up @@ -387,15 +391,23 @@ function init (client, d3) {
contextCircles.exit().remove();
};

function calcTreatmentRadius(treatment, opts) {
var CR = treatment.CR || 20;
function calcTreatmentRadius(treatment, opts, carbratio) {
var CR = treatment.CR || carbratio || 20;
var carbs = treatment.carbs || CR;
var insulin = treatment.insulin || 1;
var carbsOrInsulin = CR;
if ( treatment.carbs ) {
carbsOrInsulin = treatment.carbs;
} else if ( treatment.insulin ) {
carbsOrInsulin = treatment.insulin * CR;
}

var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / opts.scale
, R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / opts.scale
, R3 = R2 + 8 / opts.scale
, R4 = R2 + 25 / opts.scale
// R1 determines the size of the treatment dot
var R1 = Math.sqrt(carbsOrInsulin) / opts.scale
, R2 = R1
// R3/R4 determine how far from the treatment dot the labels are placed
, R3 = R1 + 8 / opts.scale
, R4 = R1 + 25 / opts.scale
;

return {
Expand All @@ -409,11 +421,15 @@ function init (client, d3) {

function prepareArc(treatment, radius) {
var arc_data = [
// white carb half-circle on top
{ 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 },
{ 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': radius.R2, 'outer': radius.R3 },
// blue insulin half-circle on bottom
{ 'element': '', 'color': '#0099ff', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': radius.R1 },
{ 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': radius.R2, 'outer': radius.R3 },
{ 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': radius.R2, 'outer': radius.R4 }
// these form a very short transparent arc along the bottom of an insulin treatment to position the label
// these used to be semicircles from 1.5708 to 4.7124, but that made the tooltip target too big
{ 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R3 },
{ 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R4 }
];

arc_data[0].outlineOnly = !treatment.carbs;
Expand All @@ -427,8 +443,14 @@ function init (client, d3) {
arc_data[1].element = arc_data[1].element + " " + treatment.foodType;
}

if (treatment.insulin > 0) {
arc_data[3].element = Math.round(treatment.insulin * 100) / 100 + ' U';
if ( treatment.insulin > 0) {
var dosage_units = Math.round(treatment.insulin * 100)/100;
var unit_of_measurement = ' U'; // One international unit of insulin (1 IU) is shown as '1 U'
if ( treatment.insulin < 1 && !treatment.carbs ) { // don't show the unit of measurement for insulin boluses < 1 without carbs (e.g. oref0 SMB's). Otherwise lot's of small insulin only dosages are often unreadable
unit_of_measurement = '';
}
// remove leading zeros to avoid overlap with adjacent boluses
arc_data[3].element = (dosage_units+"").replace(/^0/,"")+unit_of_measurement;
}

if (treatment.status) {
Expand Down Expand Up @@ -835,7 +857,8 @@ function init (client, d3) {
.style('fill', 'white');

label.append('text')
.style('font-size', 40 / opts.scale)
// reduce the treatment label font size to make it readable with SMB
.style('font-size', 30 / opts.scale)
.style('text-shadow', '0px 0px 10px rgba(0, 0, 0, 1)')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
Expand All @@ -856,11 +879,11 @@ function init (client, d3) {
renderer.drawTreatment(d, {
scale: renderer.bubbleScale()
, showLabels: true
});
}, client.sbx.data.profile.getCarbRatio(new Date()));
});
};

renderer.drawTreatment = function drawTreatment(treatment, opts) {
renderer.drawTreatment = function drawTreatment(treatment, opts, carbratio) {
if (!treatment.carbs && !treatment.insulin) {
return;
}
Expand All @@ -872,7 +895,7 @@ function init (client, d3) {
return;
}

var radius = calcTreatmentRadius(treatment, opts);
var radius = calcTreatmentRadius(treatment, opts, carbratio);
if (radius.isNaN) {
console.warn('Bad Data: Found isNaN value in treatment', treatment);
return;
Expand Down Expand Up @@ -1075,9 +1098,9 @@ function init (client, d3) {
var sign = t.first ? '▲▲▲' : '▬▬▬';
var ret;
if (t.cutting) {
ret = sign + ' ' + client.profilefunctions.profileSwitchName(t.cutting) + ' ' + '►►►' + ' ' + client.profilefunctions.profileSwitchName(t.profile) + ' ' + sign;
ret = sign + ' ' + t.cutting + ' ' + '►►►' + ' ' + t.profile + ' ' + sign;
} else {
ret = sign + ' ' + client.profilefunctions.profileSwitchName(t.profile) + ' ' + sign;
ret = sign + ' ' + t.profile + ' ' + sign;
}
return ret;
};
Expand Down
23 changes: 13 additions & 10 deletions lib/plugins/openaps.js
Expand Up @@ -300,7 +300,7 @@ function init(ctx) {
var valueParts = [
valueString('BG: ', prop.lastSuggested.bg)
, valueString(', ', prop.lastSuggested.reason)
, prop.lastSuggested.mealAssist && _.includes(selectedFields, 'meal-assist') ? ' <b>Meal Assist:</b> ' + prop.lastSuggested.mealAssist : ''
, prop.lastSuggested.sensitivityRatio ? ', <b>Sensitivity Ratio:</b> ' + prop.lastSuggested.sensitivityRatio : ''
];

if (_.includes(selectedFields, 'iob')) {
Expand All @@ -316,12 +316,11 @@ function init(ctx) {

function concatIOB (valueParts) {
if (prop.lastIOB) {
var bolussnooze = prop.lastIOB.bolussnooze || prop.lastIOB.bolusiob;
valueParts = valueParts.concat([
' IOB: '
', IOB: '
, sbx.roundInsulinForDisplayFormat(prop.lastIOB.iob) + 'U'
, prop.lastIOB.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(prop.lastIOB.basaliob) + 'U' : ''
, bolussnooze ? ', Bolus Snooze ' + sbx.roundInsulinForDisplayFormat(bolussnooze) + 'U' : ''
, prop.lastIOB.bolusiob ? ', Bolus IOB ' + sbx.roundInsulinForDisplayFormat(prop.lastIOB.bolusiob) + 'U' : ''
]);
}

Expand All @@ -331,32 +330,36 @@ function init(ctx) {
function getForecastPoints ( ) {
var points = [ ];

function toPoints (offset) {
function toPoints (offset, forecastType) {
return function toPoint (value, index) {
return {
mgdl: value
, color: '#ff00ff'
, mills: prop.lastPredBGs.moment.valueOf() + times.mins(5 * index).msecs + offset
, noFade: true
, forecastType: forecastType
};
};
}

if (prop.lastPredBGs) {
if (prop.lastPredBGs.values) {
points = points.concat(_.map(prop.lastPredBGs.values, toPoints(0)));
points = points.concat(_.map(prop.lastPredBGs.values, toPoints(0, "Values")));
}
if (prop.lastPredBGs.IOB) {
points = points.concat(_.map(prop.lastPredBGs.IOB, toPoints(3000)));
points = points.concat(_.map(prop.lastPredBGs.IOB, toPoints(3333, "IOB")));
}
if (prop.lastPredBGs.ZT) {
points = points.concat(_.map(prop.lastPredBGs.ZT, toPoints(4444, "Zero-Temp")));
}
if (prop.lastPredBGs.aCOB) {
points = points.concat(_.map(prop.lastPredBGs.aCOB, toPoints(5000)));
points = points.concat(_.map(prop.lastPredBGs.aCOB, toPoints(5555, "Accel-COB")));
}
if (prop.lastPredBGs.COB) {
points = points.concat(_.map(prop.lastPredBGs.COB, toPoints(7000)));
points = points.concat(_.map(prop.lastPredBGs.COB, toPoints(7777, "COB")));
}
if (prop.lastPredBGs.UAM) {
points = points.concat(_.map(prop.lastPredBGs.UAM, toPoints(9000)));
points = points.concat(_.map(prop.lastPredBGs.UAM, toPoints(9999, "UAM")));
}
}

Expand Down
2 changes: 1 addition & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "Nightscout",
"version": "0.10.2-release-20171026",
"version": "0.10.2-release-20171027",
"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": "AGPL-3.0",
"author": "Nightscout Team",
Expand Down
2 changes: 1 addition & 1 deletion tests/client.renderer.test.js
Expand Up @@ -19,7 +19,7 @@ describe('renderer', () => {
let mockClient = {
utils: true
, chart: { prevChartWidth: prev.width }
, foucusRangeMS: true
, focusRangeMS: true
};
it('scales correctly', () => {
renderer(mockClient, {}).bubbleScale().should.be.approximately(prev.expectedScale, MAX_DELTA);
Expand Down
1 change: 1 addition & 0 deletions views/index.html
Expand Up @@ -148,6 +148,7 @@
</div>
</div>
<ul class="focus-range">
<li data-hours="2" class="translate">2HR</li>
<li data-hours="3" class="translate">3HR</li>
<li data-hours="6" class="translate">6HR</li>
<li data-hours="12" class="translate">12HR</li>
Expand Down

0 comments on commit 9e0cc8b

Please sign in to comment.