mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
* upgrade webpack info * cleanup eslint findings * fix eslint formatter stylish error * lazy is invalid option for webpack dev middleware * upgrade webpack to 5.x * remove unsupported terser option * fix missing process polyfill dependency * try alternate proces fix * fix css image reference * add statements to import missing css files to index.html * TerserPlugin no longer required * add missing css links * remove terser Co-authored-by: Jeremy Cunningham <jpcunningh@gmail.com> Co-authored-by: Sulka Haro <sulka@sulka.net>
750 lines
26 KiB
JavaScript
750 lines
26 KiB
JavaScript
'use strict';
|
|
|
|
var _ = require('lodash');
|
|
var times = require('../times');
|
|
var d3locales = require('./d3locales');
|
|
var scrolling = false
|
|
, scrollNow = 0
|
|
, scrollBrushExtent = null
|
|
, scrollRange = null;
|
|
|
|
var PADDING_BOTTOM = 30
|
|
, OPEN_TOP_HEIGHT = 8
|
|
, CONTEXT_MAX = 420
|
|
, CONTEXT_MIN = 36
|
|
, FOCUS_MAX = 510
|
|
, FOCUS_MIN = 30;
|
|
|
|
var loadTime = Date.now();
|
|
|
|
function init (client, d3, $) {
|
|
var chart = {};
|
|
|
|
var utils = client.utils;
|
|
var renderer = client.renderer;
|
|
|
|
var defs = d3.select('body').append('svg').append('defs');
|
|
|
|
// add defs for combo boluses
|
|
var dashWidth = 5;
|
|
defs.append('pattern')
|
|
.attr('id', 'hash')
|
|
.attr('patternUnits', 'userSpaceOnUse')
|
|
.attr('width', 6)
|
|
.attr('height', 6)
|
|
.attr('x', 0)
|
|
.attr('y', 0)
|
|
.append('g')
|
|
.style('fill', 'none')
|
|
.style('stroke', '#0099ff')
|
|
.style('stroke-width', 2)
|
|
.append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth)
|
|
.append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth);
|
|
|
|
// arrow head
|
|
defs.append('marker')
|
|
.attr('id', 'arrow')
|
|
.attr('viewBox', '0 -5 10 10')
|
|
.attr('refX', 5)
|
|
.attr('refY', 0)
|
|
.attr('markerWidth', 8)
|
|
.attr('markerHeight', 8)
|
|
.attr('orient', 'auto')
|
|
.append('path')
|
|
.attr('d', 'M0,-5L10,0L0,5')
|
|
.attr('class', 'arrowHead');
|
|
|
|
var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language));
|
|
|
|
function beforeBrushStarted () {
|
|
// go ahead and move the brush because
|
|
// a single click will not execute the brush event
|
|
var now = new Date();
|
|
var dx = chart.xScale2(now) - chart.xScale2(new Date(now.getTime() - client.focusRangeMS));
|
|
|
|
var cx = d3.mouse(this)[0];
|
|
var x0 = cx - dx / 2;
|
|
var x1 = cx + dx / 2;
|
|
|
|
var range = chart.xScale2.range();
|
|
var X0 = range[0];
|
|
var X1 = range[1];
|
|
|
|
var brush = x0 < X0 ? [X0, X0 + dx] : x1 > X1 ? [X1 - dx, X1] : [x0, x1];
|
|
|
|
chart.theBrush.call(chart.brush.move, brush);
|
|
}
|
|
|
|
function brushStarted () {
|
|
// update the opacity of the context data points to brush extent
|
|
chart.context.selectAll('circle')
|
|
.data(client.entries)
|
|
.style('opacity', 1);
|
|
}
|
|
|
|
function brushEnded () {
|
|
// update the opacity of the context data points to brush extent
|
|
var selectedRange = chart.createAdjustedRange();
|
|
var from = selectedRange[0].getTime();
|
|
var to = selectedRange[1].getTime();
|
|
|
|
chart.context.selectAll('circle')
|
|
.data(client.entries)
|
|
.style('opacity', function(d) { return renderer.highlightBrushPoints(d, from, to) });
|
|
}
|
|
|
|
var extent = client.dataExtent();
|
|
|
|
var yScaleType;
|
|
if (client.settings.scaleY === 'linear') {
|
|
yScaleType = d3.scaleLinear;
|
|
} else {
|
|
yScaleType = d3.scaleLog;
|
|
}
|
|
|
|
var focusYDomain = [utils.scaleMgdl(FOCUS_MIN), utils.scaleMgdl(FOCUS_MAX)];
|
|
var contextYDomain = [utils.scaleMgdl(CONTEXT_MIN), utils.scaleMgdl(CONTEXT_MAX)];
|
|
|
|
function dynamicDomain () {
|
|
// 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
|
|
// 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(FOCUS_MIN)
|
|
, Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult))
|
|
];
|
|
}
|
|
|
|
function dynamicDomainOrElse (defaultDomain) {
|
|
if (client.entries && (client.entries.length > 0) && (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic')) {
|
|
return dynamicDomain();
|
|
} else {
|
|
return defaultDomain;
|
|
}
|
|
}
|
|
|
|
// define the parts of the axis that aren't dependent on width or height
|
|
var xScale = chart.xScale = d3.scaleTime().domain(extent);
|
|
|
|
focusYDomain = dynamicDomainOrElse(focusYDomain);
|
|
var yScale = chart.yScale = yScaleType()
|
|
.domain(focusYDomain);
|
|
|
|
var xScale2 = chart.xScale2 = d3.scaleTime().domain(extent);
|
|
|
|
contextYDomain = dynamicDomainOrElse(contextYDomain);
|
|
|
|
var yScale2 = chart.yScale2 = yScaleType()
|
|
.domain(contextYDomain);
|
|
|
|
chart.xScaleBasals = d3.scaleTime().domain(extent);
|
|
|
|
chart.yScaleBasals = d3.scaleLinear()
|
|
.domain([0, 5]);
|
|
|
|
var formatMillisecond = localeFormatter.format('.%L')
|
|
, formatSecond = localeFormatter.format(':%S')
|
|
, formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') :
|
|
localeFormatter.format('%-I:%M')
|
|
, formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') :
|
|
localeFormatter.format('%-I %p')
|
|
, formatDay = localeFormatter.format('%a %d')
|
|
, formatWeek = localeFormatter.format('%b %d')
|
|
, formatMonth = localeFormatter.format('%B')
|
|
, formatYear = localeFormatter.format('%Y');
|
|
|
|
var tickFormat = function(date) {
|
|
return (d3.timeSecond(date) < date ? formatMillisecond :
|
|
d3.timeMinute(date) < date ? formatSecond :
|
|
d3.timeHour(date) < date ? formatMinute :
|
|
d3.timeDay(date) < date ? formatHour :
|
|
d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) :
|
|
d3.timeYear(date) < date ? formatMonth :
|
|
formatYear)(date);
|
|
};
|
|
|
|
var tickValues = client.ticks(client);
|
|
|
|
chart.xAxis = d3.axisBottom(xScale)
|
|
|
|
chart.xAxis = d3.axisBottom(xScale)
|
|
.tickFormat(tickFormat)
|
|
.ticks(6);
|
|
|
|
chart.yAxis = d3.axisLeft(yScale)
|
|
.tickFormat(d3.format('d'))
|
|
.tickValues(tickValues);
|
|
|
|
chart.xAxis2 = d3.axisBottom(xScale2)
|
|
.tickFormat(tickFormat)
|
|
.ticks(6);
|
|
|
|
chart.yAxis2 = d3.axisRight(yScale2)
|
|
.tickFormat(d3.format('d'))
|
|
.tickValues(tickValues);
|
|
|
|
d3.select('tick')
|
|
.style('z-index', '10000');
|
|
|
|
// setup a brush
|
|
chart.brush = d3.brushX()
|
|
.on('start', brushStarted)
|
|
.on('brush', function brush (time) {
|
|
// layouting the graph causes a brushed event
|
|
// ignore retro data load the first two seconds
|
|
if (Date.now() - loadTime > 2000) client.loadRetroIfNeeded();
|
|
client.brushed(time);
|
|
})
|
|
.on('end', brushEnded);
|
|
|
|
chart.theBrush = null;
|
|
|
|
chart.futureOpacity = (function() {
|
|
var scale = d3.scaleLinear()
|
|
.domain([times.mins(25).msecs, times.mins(60).msecs])
|
|
.range([0.8, 0.1]);
|
|
|
|
return function(delta) {
|
|
if (delta < 0) {
|
|
return null;
|
|
} else {
|
|
return scale(delta);
|
|
}
|
|
};
|
|
})();
|
|
|
|
// create svg and g to contain the chart contents
|
|
chart.charts = d3.select('#chartContainer').append('svg')
|
|
.append('g')
|
|
.attr('class', 'chartContainer');
|
|
|
|
chart.basals = chart.charts.append('g').attr('class', 'chart-basals');
|
|
|
|
chart.focus = chart.charts.append('g').attr('class', 'chart-focus');
|
|
chart.drag = chart.focus.append('g').attr('class', 'drag-area');
|
|
|
|
// create the x axis container
|
|
chart.focus.append('g')
|
|
.attr('class', 'x axis')
|
|
.style("font-size", "16px");
|
|
|
|
// create the y axis container
|
|
chart.focus.append('g')
|
|
.attr('class', 'y axis')
|
|
.style("font-size", "16px");
|
|
|
|
chart.context = chart.charts.append('g')
|
|
.attr('class', 'chart-context');
|
|
|
|
// create the x axis container
|
|
chart.context.append('g')
|
|
.attr('class', 'x axis')
|
|
.style("font-size", "16px");
|
|
|
|
// create the y axis container
|
|
chart.context.append('g')
|
|
.attr('class', 'y axis')
|
|
.style("font-size", "16px");
|
|
|
|
chart.createBrushedRange = function() {
|
|
var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null;
|
|
|
|
var range = brushedRange && brushedRange.map(chart.xScale2.invert);
|
|
var dataExtent = client.dataExtent();
|
|
|
|
if (!brushedRange) {
|
|
// console.log('No current brushed range. Setting range to last focusRangeMS amount of available data');
|
|
range = dataExtent;
|
|
range[0] = new Date(range[1].getTime() - client.focusRangeMS);
|
|
}
|
|
|
|
var end = range[1].getTime();
|
|
if (!chart.inRetroMode()) {
|
|
end = client.now > dataExtent[1].getTime() ? client.now : dataExtent[1].getTime();
|
|
}
|
|
range[1] = new Date(end);
|
|
range[0] = new Date(end - client.focusRangeMS);
|
|
|
|
// console.log('createBrushedRange: ', brushedRange, range);
|
|
|
|
return range;
|
|
}
|
|
|
|
chart.createAdjustedRange = function() {
|
|
var adjustedRange = chart.createBrushedRange();
|
|
|
|
adjustedRange[1] = new Date(Math.max(adjustedRange[1].getTime(), client.forecastTime));
|
|
|
|
return adjustedRange;
|
|
}
|
|
|
|
chart.inRetroMode = function inRetroMode () {
|
|
var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null;
|
|
|
|
if (!brushedRange || !chart.xScale2) {
|
|
return false;
|
|
}
|
|
|
|
var maxTime = chart.xScale2.domain()[1].getTime();
|
|
var brushTime = chart.xScale2.invert(brushedRange[1]).getTime();
|
|
|
|
return brushTime < maxTime;
|
|
};
|
|
|
|
// called for initial update and updates for resize
|
|
chart.update = function update (init) {
|
|
|
|
if (client.documentHidden && !init) {
|
|
console.info('Document Hidden, not updating - ' + (new Date()));
|
|
return;
|
|
}
|
|
|
|
chart.setForecastTime();
|
|
|
|
var chartContainer = $('#chartContainer');
|
|
|
|
if (chartContainer.length < 1) {
|
|
console.warn('Unable to find element for #chartContainer');
|
|
return;
|
|
}
|
|
|
|
// get current data range
|
|
var dataRange = client.dataExtent();
|
|
var chartContainerRect = chartContainer[0].getBoundingClientRect();
|
|
var chartWidth = chartContainerRect.width;
|
|
var chartHeight = chartContainerRect.height - PADDING_BOTTOM;
|
|
|
|
// get the height of each chart based on its container size ratio
|
|
var focusHeight = chart.focusHeight = chartHeight * .7;
|
|
var contextHeight = chart.contextHeight = chartHeight * .3;
|
|
chart.basalsHeight = focusHeight / 4;
|
|
|
|
// get current brush extent
|
|
var currentRange = chart.createAdjustedRange();
|
|
var currentBrushExtent = chart.createBrushedRange();
|
|
|
|
// only redraw chart if chart size has changed
|
|
var widthChanged = (chart.prevChartWidth !== chartWidth);
|
|
if (widthChanged || (chart.prevChartHeight !== chartHeight)) {
|
|
|
|
//if rotated
|
|
if (widthChanged) {
|
|
client.browserUtils.closeLastOpenedDrawer();
|
|
}
|
|
|
|
chart.prevChartWidth = chartWidth;
|
|
chart.prevChartHeight = chartHeight;
|
|
|
|
//set the width and height of the SVG element
|
|
chart.charts.attr('width', chartWidth)
|
|
.attr('height', chartHeight + PADDING_BOTTOM);
|
|
|
|
// ranges are based on the width and height available so reset
|
|
chart.xScale.range([0, chartWidth]);
|
|
chart.xScale2.range([0, chartWidth]);
|
|
chart.xScaleBasals.range([0, chartWidth]);
|
|
chart.yScale.range([focusHeight, 0]);
|
|
chart.yScale2.range([contextHeight, 0]);
|
|
chart.yScaleBasals.range([0, focusHeight / 4]);
|
|
|
|
if (init) {
|
|
|
|
// if first run then just display axis with no transition
|
|
chart.focus.select('.x')
|
|
.attr('transform', 'translate(0,' + focusHeight + ')')
|
|
.call(chart.xAxis);
|
|
|
|
chart.focus.select('.y')
|
|
.attr('transform', 'translate(' + chartWidth + ',0)')
|
|
.call(chart.yAxis);
|
|
|
|
// if first run then just display axis with no transition
|
|
chart.context
|
|
.attr('transform', 'translate(0,' + focusHeight + ')')
|
|
|
|
chart.context.select('.x')
|
|
.attr('transform', 'translate(0,' + contextHeight + ')')
|
|
.call(chart.xAxis2);
|
|
|
|
chart.theBrush = chart.context.append('g')
|
|
.attr('class', 'x brush')
|
|
.call(chart.brush)
|
|
.call(g => g.select(".overlay")
|
|
.datum({ type: 'selection' })
|
|
.on('mousedown touchstart', beforeBrushStarted));
|
|
|
|
chart.theBrush.selectAll('rect')
|
|
.attr('y', 0)
|
|
.attr('height', contextHeight)
|
|
.attr('width', '100%');
|
|
|
|
// disable resizing of brush
|
|
chart.context.select('.x.brush').select('.overlay').style('cursor', 'move');
|
|
chart.context.select('.x.brush').selectAll('.handle')
|
|
.style('cursor', 'move');
|
|
|
|
chart.context.select('.x.brush').select('.selection')
|
|
.style('visibility', 'hidden');
|
|
|
|
// add a line that marks the current time
|
|
chart.focus.append('line')
|
|
.attr('class', 'now-line')
|
|
.attr('x1', chart.xScale(new Date(client.now)))
|
|
.attr('y1', chart.yScale(focusYDomain[0]))
|
|
.attr('x2', chart.xScale(new Date(client.now)))
|
|
.attr('y2', chart.yScale(focusYDomain[1]))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
// add a y-axis line that shows the high bg threshold
|
|
chart.focus.append('line')
|
|
.attr('class', 'high-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh)))
|
|
.style('stroke-dasharray', ('1, 6'))
|
|
.attr('stroke', '#777');
|
|
|
|
// add a y-axis line that shows the high bg threshold
|
|
chart.focus.append('line')
|
|
.attr('class', 'target-top-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
// add a y-axis line that shows the low bg threshold
|
|
chart.focus.append('line')
|
|
.attr('class', 'target-bottom-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
// add a y-axis line that shows the low bg threshold
|
|
chart.focus.append('line')
|
|
.attr('class', 'low-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow)))
|
|
.style('stroke-dasharray', ('1, 6'))
|
|
.attr('stroke', '#777');
|
|
|
|
// add a y-axis line that opens up the brush extent from the context to the focus
|
|
chart.context.append('line')
|
|
.attr('class', 'open-top')
|
|
.attr('stroke', '#111')
|
|
.attr('stroke-width', OPEN_TOP_HEIGHT);
|
|
|
|
// add a x-axis line that closes the the brush container on left side
|
|
chart.context.append('line')
|
|
.attr('class', 'open-left')
|
|
.attr('stroke', 'white');
|
|
|
|
// add a x-axis line that closes the the brush container on right side
|
|
chart.context.append('line')
|
|
.attr('class', 'open-right')
|
|
.attr('stroke', 'white');
|
|
|
|
// add a line that marks the current time
|
|
chart.context.append('line')
|
|
.attr('class', 'now-line')
|
|
.attr('x1', chart.xScale(new Date(client.now)))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale(new Date(client.now)))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
// add a y-axis line that shows the high bg threshold
|
|
chart.context.append('line')
|
|
.attr('class', 'high-line')
|
|
.attr('x1', chart.xScale(dataRange[0]))
|
|
.attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.attr('x2', chart.xScale(dataRange[1]))
|
|
.attr('y2', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
// add a y-axis line that shows the low bg threshold
|
|
chart.context.append('line')
|
|
.attr('class', 'low-line')
|
|
.attr('x1', chart.xScale(dataRange[0]))
|
|
.attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.attr('x2', chart.xScale(dataRange[1]))
|
|
.attr('y2', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.style('stroke-dasharray', ('3, 3'))
|
|
.attr('stroke', 'grey');
|
|
|
|
} else {
|
|
|
|
// for subsequent updates use a transition to animate the axis to the new position
|
|
|
|
chart.focus.select('.x')
|
|
.attr('transform', 'translate(0,' + focusHeight + ')')
|
|
.call(chart.xAxis);
|
|
|
|
chart.focus.select('.y')
|
|
.attr('transform', 'translate(' + chartWidth + ', 0)')
|
|
.call(chart.yAxis);
|
|
|
|
chart.context
|
|
.attr('transform', 'translate(0,' + focusHeight + ')')
|
|
|
|
chart.context.select('.x')
|
|
.attr('transform', 'translate(0,' + contextHeight + ')')
|
|
.call(chart.xAxis2);
|
|
|
|
chart.basals;
|
|
|
|
// reset brush location
|
|
chart.theBrush.selectAll('rect')
|
|
.attr('y', 0)
|
|
.attr('height', contextHeight);
|
|
|
|
// console.log('chart.update(): Redrawing old brush with new dimensions: ', currentBrushExtent);
|
|
|
|
// redraw old brush with new dimensions
|
|
chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2));
|
|
|
|
// transition lines to correct location
|
|
chart.focus.select('.high-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh)));
|
|
|
|
chart.focus.select('.target-top-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)));
|
|
|
|
chart.focus.select('.target-bottom-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)));
|
|
|
|
chart.focus.select('.low-line')
|
|
.attr('x1', chart.xScale.range()[0])
|
|
.attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow)))
|
|
.attr('x2', chart.xScale.range()[1])
|
|
.attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow)));
|
|
|
|
// transition open-top line to correct location
|
|
chart.context.select('.open-top')
|
|
.attr('x1', chart.xScale2(currentRange[0]))
|
|
.attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1)
|
|
.attr('x2', chart.xScale2(currentRange[1]))
|
|
.attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1);
|
|
|
|
// transition open-left line to correct location
|
|
chart.context.select('.open-left')
|
|
.attr('x1', chart.xScale2(currentRange[0]))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale2(currentRange[0]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]));
|
|
|
|
// transition open-right line to correct location
|
|
chart.context.select('.open-right')
|
|
.attr('x1', chart.xScale2(currentRange[1]))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale2(currentRange[1]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]));
|
|
|
|
// transition high line to correct location
|
|
chart.context.select('.high-line')
|
|
.attr('x1', chart.xScale2(dataRange[0]))
|
|
.attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)))
|
|
.attr('x2', chart.xScale2(dataRange[1]))
|
|
.attr('y2', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop)));
|
|
|
|
// transition low line to correct location
|
|
chart.context.select('.low-line')
|
|
.attr('x1', chart.xScale2(dataRange[0]))
|
|
.attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)))
|
|
.attr('x2', chart.xScale2(dataRange[1]))
|
|
.attr('y2', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom)));
|
|
}
|
|
}
|
|
|
|
chart.updateContext(dataRange);
|
|
|
|
chart.xScaleBasals.domain(dataRange);
|
|
|
|
// console.log('chart.update(): Redrawing brush due to update: ', currentBrushExtent);
|
|
|
|
chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2));
|
|
};
|
|
|
|
chart.updateContext = function(dataRange_) {
|
|
if (client.documentHidden) {
|
|
console.info('Document Hidden, not updating - ' + (new Date()));
|
|
return;
|
|
}
|
|
|
|
// get current data range
|
|
var dataRange = dataRange_ || client.dataExtent();
|
|
|
|
// update domain
|
|
chart.xScale2.domain(dataRange);
|
|
|
|
renderer.addContextCircles();
|
|
|
|
// update x axis domain
|
|
chart.context.select('.x').call(chart.xAxis2);
|
|
};
|
|
|
|
function scrollUpdate () {
|
|
var nowDate = scrollNow;
|
|
|
|
var currentBrushExtent = scrollBrushExtent;
|
|
var currentRange = scrollRange;
|
|
|
|
chart.setForecastTime();
|
|
|
|
chart.xScale.domain(currentRange);
|
|
|
|
focusYDomain = dynamicDomainOrElse(focusYDomain);
|
|
|
|
chart.yScale.domain(focusYDomain);
|
|
chart.xScaleBasals.domain(currentRange);
|
|
|
|
// remove all insulin/carb treatment bubbles so that they can be redrawn to correct location
|
|
d3.selectAll('.path').remove();
|
|
|
|
// transition open-top line to correct location
|
|
chart.context.select('.open-top')
|
|
.attr('x1', chart.xScale2(currentRange[0]))
|
|
.attr('y1', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1)
|
|
.attr('x2', chart.xScale2(currentRange[1]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1);
|
|
|
|
// transition open-left line to correct location
|
|
chart.context.select('.open-left')
|
|
.attr('x1', chart.xScale2(currentRange[0]))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale2(currentRange[0]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]));
|
|
|
|
// transition open-right line to correct location
|
|
chart.context.select('.open-right')
|
|
.attr('x1', chart.xScale2(currentRange[1]))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale2(currentRange[1]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]));
|
|
|
|
chart.focus.select('.now-line')
|
|
.attr('x1', chart.xScale(nowDate))
|
|
.attr('y1', chart.yScale(focusYDomain[0]))
|
|
.attr('x2', chart.xScale(nowDate))
|
|
.attr('y2', chart.yScale(focusYDomain[1]));
|
|
|
|
chart.context.select('.now-line')
|
|
.attr('x1', chart.xScale2(currentBrushExtent[1]))
|
|
.attr('y1', chart.yScale2(contextYDomain[0]))
|
|
.attr('x2', chart.xScale2(currentBrushExtent[1]))
|
|
.attr('y2', chart.yScale2(contextYDomain[1]));
|
|
|
|
// update x,y axis
|
|
chart.focus.select('.x.axis').call(chart.xAxis);
|
|
chart.focus.select('.y.axis').call(chart.yAxis);
|
|
|
|
renderer.addBasals(client);
|
|
|
|
renderer.addFocusCircles();
|
|
renderer.addTreatmentCircles(nowDate);
|
|
renderer.addTreatmentProfiles(client);
|
|
renderer.drawTreatments(client);
|
|
|
|
// console.log('scrollUpdate(): Redrawing brush due to update: ', currentBrushExtent);
|
|
|
|
chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2));
|
|
|
|
scrolling = false;
|
|
}
|
|
|
|
chart.scroll = function scroll (nowDate) {
|
|
scrollNow = nowDate;
|
|
scrollBrushExtent = chart.createBrushedRange();
|
|
scrollRange = chart.createAdjustedRange();
|
|
|
|
if (!scrolling) {
|
|
requestAnimationFrame(scrollUpdate);
|
|
}
|
|
|
|
scrolling = true;
|
|
};
|
|
|
|
chart.getMaxForecastMills = function getMaxForecastMills () {
|
|
// limit lookahead to the same as lookback
|
|
var selectedRange = chart.createBrushedRange();
|
|
var to = selectedRange[1].getTime();
|
|
return to + client.focusRangeMS;
|
|
};
|
|
|
|
chart.getForecastData = function getForecastData () {
|
|
|
|
var maxForecastAge = chart.getMaxForecastMills();
|
|
var pointTypes = client.settings.showForecast.split(' ');
|
|
|
|
var points = pointTypes.reduce( function (points, type) {
|
|
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
|
return points.concat(client.sbx.pluginBase.forecastPoints[type] || []);
|
|
}, [] );
|
|
|
|
return _.filter(points, function isShown (point) {
|
|
return point.mills < maxForecastAge;
|
|
});
|
|
|
|
};
|
|
|
|
chart.setForecastTime = function setForecastTime () {
|
|
|
|
if (client.sbx.pluginBase.forecastPoints) {
|
|
var shownForecastPoints = chart.getForecastData();
|
|
|
|
// Get maximum time we will allow projected forward in time
|
|
// based on the number of hours the user has selected to show.
|
|
var maxForecastMills = chart.getMaxForecastMills();
|
|
|
|
var selectedRange = chart.createBrushedRange();
|
|
var to = selectedRange[1].getTime();
|
|
|
|
// Default min forecast projection times to the default amount of time to forecast
|
|
var minForecastMills = to + client.defaultForecastTime;
|
|
var availForecastMills = 0;
|
|
|
|
// Determine what the maximum forecast time is that is available in the forecast data
|
|
if (shownForecastPoints.length > 0) {
|
|
availForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills }));
|
|
}
|
|
|
|
// Limit the amount shown to the maximum time allowed to be projected forward based
|
|
// on the number of hours the user has selected to show
|
|
var forecastMills = Math.min(availForecastMills, maxForecastMills);
|
|
|
|
// Don't allow the forecast time to go below the minimum forecast time
|
|
client.forecastTime = Math.max(forecastMills, minForecastMills);
|
|
}
|
|
};
|
|
|
|
return chart;
|
|
}
|
|
|
|
module.exports = init;
|