Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6249ce0
Merge pull request #5 from nightscout/master
tim2000s Feb 15, 2018
8de5dee
Merge pull request #9 from nightscout/master
tim2000s Nov 19, 2018
7f44d38
Merge pull request #11 from nightscout/master
tim2000s Feb 8, 2019
345e364
Merge pull request #12 from nightscout/master
tim2000s Aug 16, 2019
8f12371
Merge pull request #13 from nightscout/master
tim2000s Oct 30, 2019
02043b0
Merge pull request #14 from nightscout/master
tim2000s Dec 18, 2019
a0adfad
Merge pull request #17 from nightscout/master
tim2000s Sep 13, 2020
dd9be6d
Merge pull request #19 from nightscout/master
tim2000s Sep 21, 2020
ffca32d
Merge pull request #20 from nightscout/master
tim2000s Nov 18, 2020
cac29e4
Merge pull request #21 from nightscout/master
tim2000s Mar 22, 2021
4a00bb8
Merge pull request #22 from nightscout/master
tim2000s Mar 11, 2023
9fc44a8
Merge pull request #24 from nightscout/master
tim2000s Oct 19, 2023
18a36e2
Merge branch 'nightscout:master' into master
tim2000s Oct 26, 2023
0727987
Merge branch 'nightscout:master' into master
tim2000s Jun 4, 2026
3a4621a
chore(deps): upgrade D3 v5 -> v7
tim2000s Jun 16, 2026
9c75e46
feat(reports): Statistics Summary (AGP) + modernize all report charts
tim2000s Jun 16, 2026
afb778d
feat(reports): copy/export summary on Statistics Summary (AGP)
tim2000s Jun 16, 2026
2179825
fix(reports): correct hourly box-and-whisker whiskers (percentile, no…
tim2000s Jun 16, 2026
06da27d
test: load d3 from the bundle in the headless harness (Node < 25 fix)
tim2000s Jun 16, 2026
312e9cc
test(admintools): load d3 from the bundle instead of require('d3')
tim2000s Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions lib/client/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ function init (client, d3, $) {

var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language));

function beforeBrushStarted () {
function beforeBrushStarted (event) {
// 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];
// d3 v5's mouse helper unwrapped touch events automatically; d3.pointer does not,
// so pass the first changed touch for touchstart events
var pointerSource = event.changedTouches ? event.changedTouches[0] : event;
var cx = d3.pointer(pointerSource, this)[0];
var x0 = cx - dx / 2;
var x1 = cx + dx / 2;

Expand Down Expand Up @@ -196,11 +199,13 @@ function init (client, d3, $) {
// setup a brush
chart.brush = d3.brushX()
.on('start', brushStarted)
.on('brush', function brush (time) {
.on('brush', function brush () {
// layouting the graph causes a brushed event
// ignore retro data load the first two seconds
// (in d3 v6+ the handler receives the brush event as its first
// argument; client.brushed() reads the selection itself and takes no args)
if (Date.now() - loadTime > 2000) client.loadRetroIfNeeded();
client.brushed(time);
client.brushed();
})
.on('end', brushEnded);

Expand Down
50 changes: 25 additions & 25 deletions lib/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ function init (client, d3) {
return radius / focusRangeAdjustment();
};

function tooltipLeft () {
function tooltipLeft (event) {
var windowWidth = $(client.tooltip.node()).parent().parent().width();
var left = d3.event.pageX + TOOLTIP_WIDTH < windowWidth ? d3.event.pageX : windowWidth - TOOLTIP_WIDTH - 10;
var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH - 10;
return left + 'px';
}

Expand Down Expand Up @@ -128,7 +128,7 @@ function init (client, d3) {
return sel;
}

function focusCircleTooltip (d) {
function focusCircleTooltip (event, d) {
if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') {
return;
}
Expand All @@ -154,8 +154,8 @@ function init (client, d3) {
(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(getOrAddDate(d)))
.style('left', tooltipLeft())
.style('top', (d3.event.pageY + 15) + 'px');
.style('left', tooltipLeft(event))
.style('top', (event.pageY + 15) + 'px');
}

// CGM data
Expand Down Expand Up @@ -337,11 +337,11 @@ function init (client, d3) {
// if new circle then just display
prepareTreatCircles(treatCircles.enter().append('circle'))
.attr('class', 'treatment-dot')
.on('mouseover', function(d) {
.on('mouseover', function(event, d) {
client.tooltip.style('display', 'block');
client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d))
.style('left', tooltipLeft())
.style('top', (d3.event.pageY + 15) + 'px');
.style('left', tooltipLeft(event))
.style('top', (event.pageY + 15) + 'px');
})
.on('mouseout', hideTooltip);

Expand Down Expand Up @@ -434,11 +434,11 @@ function init (client, d3) {
var newRects = treatRects.enter().append('g')
.attr('class', 'g-duration')
.attr('transform', rectTranslate)
.on('mouseover', function(d) {
.on('mouseover', function(event, d) {
client.tooltip.style('display', 'block');
client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d))
.style('left', tooltipLeft())
.style('top', (d3.event.pageY + 15) + 'px');
.style('left', tooltipLeft(event))
.style('top', (event.pageY + 15) + 'px');
})
.on('mouseout', hideTooltip);

Expand Down Expand Up @@ -631,7 +631,7 @@ function init (client, d3) {
return html;
}

function treatmentTooltip () {
function treatmentTooltip (event) {
var glucose = treatment.glucose;
if (client.settings.units != client.ddata.profile.getUnits()) {
glucose *= (client.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL);
Expand All @@ -653,8 +653,8 @@ function init (client, d3) {
(treatment.notes ? '<strong>' + translate('Notes') + ':</strong> ' + treatment.notes : '') +
boluscalcTooltip(treatment)
)
.style('left', tooltipLeft())
.style('top', (d3.event.pageY + 15) + 'px');
.style('left', tooltipLeft(event))
.style('top', (event.pageY + 15) + 'px');
}

var newTime;
Expand All @@ -663,13 +663,13 @@ function init (client, d3) {
var carbsRect = { x: 0, y: 0, width: 0, height: 0 };
var operation;
renderer.drag = d3.drag()
.on('start', function() {
.on('start', function(event) {
//console.log(treatment);
var windowWidth = $(client.tooltip.node()).parent().parent().width();
var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10;
var left = event.x + TOOLTIP_WIDTH < windowWidth ? event.x : windowWidth - TOOLTIP_WIDTH - 10;
client.tooltip.style('display', 'block')
.style('left', left + 'px')
.style('top', (d3.event.pageY ? d3.event.pageY + 15 : 40) + 'px');
.style('top', (event.sourceEvent && event.sourceEvent.pageY ? event.sourceEvent.pageY + 15 : 40) + 'px');

deleteRect = {
x: 0
Expand Down Expand Up @@ -757,14 +757,14 @@ function init (client, d3) {
chart().basals.attr('display', 'none');

operation = 'Move';
var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width'));
var x = Math.min(Math.max(0, event.x), chart().charts.attr('width'));
newTime = new Date(chart().xScale.invert(x));
})
.on('drag', function() {
//console.log(d3.event);
.on('drag', function(event) {
//console.log(event);
client.tooltip.style('display', 'block');
var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width'));
var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight);
var x = Math.min(Math.max(0, event.x), chart().charts.attr('width'));
var y = Math.min(Math.max(0, event.y), chart().focusHeight);

operation = 'Move';
if (isInRect(x, y, deleteRect) && isInRect(x, y, insulinRect)) {
Expand Down Expand Up @@ -1267,11 +1267,11 @@ function init (client, d3) {
'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')';
})
.text(generateText)
.on('mouseover', function(d) {
.on('mouseover', function(event, d) {
client.tooltip.style('display', 'block');
client.tooltip.html(profileTooltip(d))
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY + 15) + 'px');
.style('left', (event.pageX) + 'px')
.style('top', (event.pageY + 15) + 'px');
})
.on('mouseout', hideTooltip);

Expand Down
76 changes: 76 additions & 0 deletions lib/report/reportclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ var init = function init () {
var datastorage = {};
var daystoshow = {};
var sorteddaystoshow = [];
// Tracks which report tabs have already been drawn for the currently loaded
// data, and the options used, so switching tabs can lazily render from the
// already-loaded data instead of forcing a reload. Reset whenever the data
// is reloaded (i.e. when the time range changes and the report is re-run).
var renderedPlugins = {};
var lastShownOptions = null;

var targetBGdefault = {
'mg/dl': {
Expand All @@ -46,6 +52,40 @@ var init = function init () {

prepareGUI();

// Auto-generate the default (7-day) report on load so the Statistics
// Summary tab shows data immediately, without a manual "Show" click. The
// date range already defaults to the last 7 days with the date filter
// enabled, so we just need to trigger show() once the data is ready.
//
// show() needs profile/timezone data, which only becomes available on
// client.sbx.data after the first socket data-update cycle. Poll until it
// is ready, capped (~30s) so we never spin forever. The cap also makes this
// safe in the headless test harness: there setTimeout runs synchronously
// and the data is loaded later by the test itself, so the poll simply
// exhausts its attempts during init and gives up without rendering, leaving
// the test in full control. This lives in the bundle (not the static
// reportinit.js, which is not cache-busted) so it updates reliably on deploy.
var autoShowAttempts = 0;
(function waitForProfileThenShow () {
autoShowAttempts++;
var profileReady = client.sbx && client.sbx.data && client.sbx.data.profile
&& typeof client.sbx.data.profile.getTimezone === 'function';
if (profileReady) {
try {
show();
} catch (err) {
console.error('Auto-show of report failed', err);
}
} else if (autoShowAttempts < 120) {
window.setTimeout(waitForProfileThenShow, 250);
} else {
// Data never became ready within the window; leave the report idle for
// a manual "Show". Info-level so the test harness (which intentionally
// exhausts the poll) and slow loads don't surface a warning.
console.info('Auto-show: report data not ready in time; awaiting manual Show');
}
})();

// ****** FOOD CODE START ******
var food_categories = [];
var food_list = [];
Expand Down Expand Up @@ -485,12 +525,16 @@ var init = function init () {

$('#rp_show').css('display', 'none');
daystoshow = {};
// New data is being loaded for this (possibly new) time range, so any
// previously rendered tabs are now stale and must be redrawn on demand.
renderedPlugins = {};

datefilter();
return maybePrevent(event);
}

function showreports (options) {
lastShownOptions = options;
// prepare some data used in more reports
datastorage.allstatsrecords = [];
datastorage.alldays = 0;
Expand Down Expand Up @@ -543,6 +587,7 @@ var init = function init () {
console.log('Skipping ', plugin.name);
} else {
plugin.report(datastorage, sorteddaystoshow, options);
renderedPlugins[plugin.name] = true;
}

if (!$('#' + plugin.name).hasClass('selected')) {
Expand All @@ -568,9 +613,40 @@ var init = function init () {

$('.tabplaceholder').css('display', 'none');
$('#' + id + '-placeholder').css('display', '');

// Lazily render this tab from the already-loaded data the first time it is
// viewed. The heavy per-day reports (daytoday, weektoweek, treatments,
// loopalyzer) are skipped during the initial render for speed; this draws
// them on demand without reloading. Tabs already drawn for the current
// time range are left as-is.
maybeRenderPlugin(id);

return maybePrevent(event);
}

function maybeRenderPlugin (name) {
if (renderedPlugins[name]) { return; } // already drawn for the current data
if (!lastShownOptions || !sorteddaystoshow.length) { return; } // no data loaded yet
var plugin = report_plugins(name);
if (!plugin || typeof plugin.report !== 'function') { return; }

// Mark as rendered synchronously so a rapid second click on the same tab
// (before the deferred render fires) cannot queue a duplicate draw.
renderedPlugins[name] = true;
$('#info').html('<b>' + translate('Rendering') + ' ...</b>');
// Defer so the tab switch paints first; the placeholder is already visible
// (jquery.plot will not draw into a hidden div).
window.setTimeout(function renderOnDemand () {
try {
plugin.report(datastorage, sorteddaystoshow, lastShownOptions);
} catch (err) {
renderedPlugins[name] = false; // allow a retry on the next activation
console.error('On-demand render of ' + name + ' failed', err);
}
$('#info').html('');
}, 0);
}

function loadData (day, options, callback) {
// check for loaded data
if ((options.openAps || options.predicted || options.iob || options.cob) && datastorage[day] && !datastorage[day].devicestatus.length) {
Expand Down
Loading
Loading