nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
750 lines (587 loc) • 27.4 kB
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;