Skip to content

Commit f4bc155

Browse files
Merge pull request #481 from tidepool-org/WEB-3688-twiist-suppressed-basal-data
[WEB-3688] Generate suppressed basal data for twiist
2 parents 51c7243 + fbfdb4e commit f4bc155

File tree

5 files changed

+557
-3
lines changed

5 files changed

+557
-3
lines changed

data/types.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export class Basal extends Common {
9595
this.deliveryType = opts.deliveryType;
9696
this.deviceTime = opts.deviceTime;
9797
this.duration = opts.duration;
98+
this.origin = opts.origin;
9899
this.rate = opts.rate;
99100
this.scheduleName = opts.scheduleName;
100101
this.annotations = opts.annotations;
@@ -104,7 +105,9 @@ export class Basal extends Common {
104105

105106
this.time = opts.time || this.makeTime();
106107
this.timezoneOffset = this.makeTimezoneOffset();
108+
if (!opts.raw) this.time = Date.parse(this.time);
107109
if (!opts.raw) this.normalTime = this.makeNormalTime();
110+
if (!opts.raw) this.deviceTime = Date.parse(this.deviceTime);
108111
if (!opts.raw) this.normalEnd = this.normalTime + this.duration;
109112
}
110113
}
@@ -229,7 +232,7 @@ export class Settings extends Common {
229232
},
230233
});
231234

232-
this.type = 'settings';
235+
this.type = 'pumpSettings';
233236

234237
this.activeSchedule = opts.activeSchedule;
235238
this.basalSchedules = opts.basalSchedules;
@@ -238,6 +241,7 @@ export class Settings extends Common {
238241
this.deviceTime = opts.deviceTime;
239242
this.insulinSensitivity = opts.insulinSensitivity;
240243
this.units = opts.units;
244+
this.id = opts.id;
241245

242246
this.time = this.makeTime();
243247
this.timezoneOffset = this.makeTimezoneOffset();
@@ -249,6 +253,7 @@ export class Settings extends Common {
249253
if (this.bgTarget.high) this.bgTarget.high = this.bgTarget.high / MGDL_PER_MMOLL;
250254
} else {
251255
this.normalTime = this.makeNormalTime();
256+
this.time = Date.parse(this.time);
252257
}
253258
}
254259
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"node": "20.8.0"
55
},
66
"packageManager": "[email protected]",
7-
"version": "1.46.0",
7+
"version": "1.47.0-web-3688-twiist-suppressed-basal-data.2",
88
"description": "Tidepool data visualization for diabetes device data.",
99
"keywords": [
1010
"data visualization"

src/components/common/tooltips/Tooltip.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
pointer-events: none;
2424
position: absolute;
2525
border-radius: 4px;
26+
z-index: 1000;
2627
}
2728

2829
.content {

src/utils/DataUtil.js

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ export class DataUtil {
133133
_.each(data, this.joinBolusAndDosingDecision);
134134
this.endTimer('joinBolusAndDosingDecision');
135135

136-
// Filter out any data that failed validation, and and duplicates by `id`
136+
// Add missing suppressed basals to select basal datums
137+
this.startTimer('addMissingSuppressedBasals');
138+
this.addMissingSuppressedBasals(data);
139+
this.endTimer('addMissingSuppressedBasals');
140+
141+
// Filter out any data that failed validation, and any duplicates by `id`
137142
this.startTimer('filterValidData');
138143
this.clearFilters();
139144
const validData = _.uniqBy(data, 'id');
@@ -337,6 +342,191 @@ export class DataUtil {
337342
}
338343
};
339344

345+
addMissingSuppressedBasals = data => {
346+
// Mapping function to get the basal schedules for a given pumpSettings datum,
347+
// ordered by start time in descending order for easier comparison with basals
348+
const getOrderedPumpSettingsSchedules = ({ activeSchedule, basalSchedules, deviceId, time }) => ({
349+
basalSchedule: _.orderBy(basalSchedules?.[activeSchedule], 'start', 'desc'),
350+
time,
351+
deviceId,
352+
});
353+
354+
const shouldGenerateSuppressedBasal = (d) => (
355+
d.type === 'basal' &&
356+
!d.suppressed &&
357+
_.includes(['automated', 'temp'], d.deliveryType) &&
358+
isTwiistLoop(d)
359+
);
360+
361+
// Get the pump settings datums ordered by start time in descending order
362+
const pumpSettingsByStartTimes = _.orderBy(
363+
_.map(_.values(this.pumpSettingsDatumsByIdMap), getOrderedPumpSettingsSchedules),
364+
'time',
365+
'desc'
366+
);
367+
368+
// We will need add additional basals when we split existing basals that overlap with pump settings schedules
369+
const basalsToAdd = [];
370+
371+
_.each(data, d => {
372+
if (shouldGenerateSuppressedBasal(d)) {
373+
// Get the pump settings datum that is active at the time of the basal by grabbing the first
374+
// datum with a time less than or equal to the basal's time
375+
const pumpSettingsDatum = _.find(pumpSettingsByStartTimes, ps => ps.deviceId === d.deviceId && ps.time <= d.time);
376+
const activeSchedule = pumpSettingsDatum?.basalSchedule;
377+
378+
if (!activeSchedule?.length) {
379+
// No schedule available, skip this basal
380+
return;
381+
}
382+
383+
// Calculate the end time of the basal
384+
const basalEndTime = d.time + (d.duration || 0);
385+
386+
// Get the msPer24 for the utc start and end times of the basal datum, then adjust for timezone offset
387+
const timezoneOffsetMs = (d.timezoneOffset || 0) * MS_IN_MIN;
388+
const basalMsPer24WithOffset = {
389+
start: getMsPer24(d.time) + timezoneOffsetMs,
390+
end: getMsPer24(basalEndTime) + timezoneOffsetMs,
391+
};
392+
393+
// If the offset msPer24 is negative, as can happen with negative timezone offsets,
394+
// we need to add a day to adjust it to be within the 24-hour range
395+
if (basalMsPer24WithOffset.start < 0) basalMsPer24WithOffset.start += MS_IN_DAY;
396+
if (basalMsPer24WithOffset.end < 0) basalMsPer24WithOffset.end += MS_IN_DAY;
397+
398+
// If end time crosses midnight (is less than start), extend it to next day
399+
if (basalMsPer24WithOffset.end < basalMsPer24WithOffset.start) {
400+
basalMsPer24WithOffset.end += MS_IN_DAY;
401+
}
402+
403+
// Find all schedule segments that this basal overlaps
404+
const currentDaySegments = [];
405+
const nextDaySegments = [];
406+
407+
// Sort schedule by start time ascending for easier processing
408+
const sortedSchedule = _.sortBy(activeSchedule, 'start');
409+
410+
for (let i = 0; i < sortedSchedule.length; i++) {
411+
const segment = sortedSchedule[i];
412+
const nextSegment = sortedSchedule[i + 1];
413+
414+
const segmentStart = segment.start;
415+
const segmentEnd = nextSegment ? nextSegment.start : MS_IN_DAY;
416+
417+
// Check if basal overlaps with this segment in the current day
418+
const basalOverlapsCurrentDay = (
419+
basalMsPer24WithOffset.start < segmentEnd &&
420+
basalMsPer24WithOffset.end > segmentStart
421+
);
422+
423+
if (basalOverlapsCurrentDay) {
424+
currentDaySegments.push({
425+
...segment,
426+
segmentEnd,
427+
overlapStart: Math.max(basalMsPer24WithOffset.start, segmentStart),
428+
overlapEnd: Math.min(basalMsPer24WithOffset.end, segmentEnd)
429+
});
430+
}
431+
432+
// Also check if basal overlaps with this segment when extended to next day
433+
// This handles cases where the basal crosses midnight
434+
if (basalMsPer24WithOffset.end > MS_IN_DAY) {
435+
const extendedSegmentStart = segmentStart + MS_IN_DAY;
436+
const extendedSegmentEnd = segmentEnd + MS_IN_DAY;
437+
438+
const basalOverlapsNextDay = (
439+
basalMsPer24WithOffset.start < extendedSegmentEnd &&
440+
basalMsPer24WithOffset.end > extendedSegmentStart
441+
);
442+
443+
if (basalOverlapsNextDay) {
444+
nextDaySegments.push({
445+
...segment,
446+
segmentEnd: extendedSegmentEnd,
447+
overlapStart: Math.max(basalMsPer24WithOffset.start, extendedSegmentStart),
448+
overlapEnd: Math.min(basalMsPer24WithOffset.end, extendedSegmentEnd)
449+
});
450+
}
451+
}
452+
}
453+
454+
// Combine segments in chronological order: current day first, then next day
455+
const overlappingSegments = [...currentDaySegments, ...nextDaySegments];
456+
457+
// It's expected that a basal will overlap with at least one segment
458+
if (overlappingSegments.length === 1) {
459+
// Simple case: basal is within a single schedule segment
460+
const segment = overlappingSegments[0];
461+
d.suppressed = {
462+
...d,
463+
id: `${d.id}_suppressed`,
464+
deliveryType: 'scheduled',
465+
rate: segment.rate || 0,
466+
};
467+
} else if (overlappingSegments.length > 1) {
468+
// Complex case: basal crosses schedule boundaries, need to split
469+
const originalDuration = d.duration || 0;
470+
const basalDurationMs24 = basalMsPer24WithOffset.end - basalMsPer24WithOffset.start;
471+
472+
// Update the original basal to cover only the first segment
473+
const firstSegment = overlappingSegments[0];
474+
const firstSegmentMs24Duration = firstSegment.overlapEnd - firstSegment.overlapStart;
475+
const firstSegmentDuration = (firstSegmentMs24Duration / basalDurationMs24) * originalDuration;
476+
477+
d.duration = firstSegmentDuration;
478+
d.suppressed = {
479+
...d,
480+
id: `${d.id}_suppressed`,
481+
deliveryType: 'scheduled',
482+
rate: firstSegment.rate || 0,
483+
duration: firstSegmentDuration,
484+
time: d.time, // Keep original time
485+
};
486+
487+
// Create additional basal datums for the remaining segments
488+
let cumulativeDuration = firstSegmentDuration;
489+
490+
for (let i = 1; i < overlappingSegments.length; i++) {
491+
const segment = overlappingSegments[i];
492+
const segmentMs24Duration = segment.overlapEnd - segment.overlapStart;
493+
const segmentDuration = (segmentMs24Duration / basalDurationMs24) * originalDuration;
494+
495+
const newBasal = {
496+
...d,
497+
id: `${d.id}_split_${i}`,
498+
time: d.time + cumulativeDuration,
499+
duration: segmentDuration,
500+
suppressed: {
501+
...d,
502+
id: `${d.id}_suppressed_split_${i}`,
503+
deliveryType: 'scheduled',
504+
rate: segment.rate || 0,
505+
duration: segmentDuration,
506+
time: d.time + cumulativeDuration,
507+
}
508+
};
509+
510+
// Update deviceTime if it exists
511+
if (d.deviceTime) {
512+
newBasal.deviceTime = d.deviceTime + cumulativeDuration;
513+
newBasal.suppressed.deviceTime = newBasal.deviceTime;
514+
}
515+
516+
basalsToAdd.push(newBasal);
517+
cumulativeDuration += segmentDuration;
518+
}
519+
} else {
520+
// Fallback: no overlapping segments found (shouldn't happen normally)
521+
this.log('Warning: No overlapping segments found for basal', d.id);
522+
}
523+
}
524+
});
525+
526+
// Add the new split basal datums to the data array
527+
data.push(...basalsToAdd);
528+
};
529+
340530
/**
341531
* Medtronic 5 and 7 series (which always have a deviceId starting with 'MedT-') carb exchange
342532
* data is converted to carbs at a rounded 1:15 ratio in the uploader, and needs to be

0 commit comments

Comments
 (0)