Skip to content

Commit ad10e09

Browse files
Merge pull request #485 from tidepool-org/WEB-3687-new-dosing-decision-extended-duration-props
[WEB-3687|WEB-3697|WEB-3698] Handle twiist dosing decision extended bolus props + small tweaks for normal and oneButton
2 parents 22af47c + 0ab33a6 commit ad10e09

File tree

8 files changed

+178
-37
lines changed

8 files changed

+178
-37
lines changed

data/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ export class DosingDecision extends Common {
426426
}],
427427
deviceTime: this.makeDeviceTime(),
428428
reason: 'normalBolus',
429-
recommendedBolus: { normal: '2' },
429+
recommendedBolus: { amount: '2' },
430430
requestedBolus: { normal: '1.5' },
431431
units: MGDL_UNITS,
432432
});

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.1-web-3698-dosing-decision-bolus-normal",
7+
"version": "1.47.0-web-3687-new-dosing-decision-extended-duration-props.2",
88
"description": "Tidepool data visualization for diabetes device data.",
99
"keywords": [
1010
"data visualization"

src/components/daily/bolustooltip/BolusTooltip.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -405,19 +405,15 @@ class BolusTooltip extends PureComponent {
405405
}
406406

407407
render() {
408-
const isAutomated = _.get(this.props.bolus, 'subType') === 'automated';
409-
const isOneButton = bolusUtils.isOneButton(this.props.bolus);
410-
const tailColor = this.props.tailColor || isAutomated ? colors.bolusAutomated : colors.bolus;
411-
412-
const borderColor = this.props.borderColor || isAutomated
413-
? colors.bolusAutomated
414-
: colors.bolus;
408+
const { automated, oneButton } = bolusUtils.getBolusFromInsulinEvent(this.props.bolus)?.tags || {};
409+
const tailColor = this.props.tailColor ?? (automated ? colors.bolusAutomated : colors.bolus);
410+
const borderColor = this.props.borderColor ?? (automated ? colors.bolusAutomated : colors.bolus);
415411

416412
const title = (
417413
<div className={styles.title}>
418414
<div className={styles.types}>
419-
{isOneButton && <div>{this.deviceLabels[ONE_BUTTON_BOLUS]}</div>}
420-
{isAutomated && <div>{this.deviceLabels[AUTOMATED_BOLUS]}</div>}
415+
{oneButton && <div>{this.deviceLabels[ONE_BUTTON_BOLUS]}</div>}
416+
{automated && <div>{this.deviceLabels[AUTOMATED_BOLUS]}</div>}
421417
</div>
422418
{formatLocalizedFromUTC(this.props.bolus.normalTime, this.props.timePrefs, 'h:mm a')}
423419
</div>

src/utils/DataUtil.js

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,20 @@ export class DataUtil {
226226
d.sampleInterval = sampleInterval;
227227
}
228228

229-
// Use normal instead of deprecated amount for recommendedBolus and requestedBolus
230229
if (d.type === 'dosingDecision') {
231-
_.each([d.recommendedBolus, d.requestedBolus], (bolus) => {
232-
if (_.isObject(bolus)) {
233-
bolus.normal = _.get(bolus, 'normal', _.get(bolus, 'amount'));
234-
delete bolus.amount;
235-
}
236-
});
230+
// Use `normal` instead of deprecated `amount` for requestedBolus
231+
if (!d.requestedBolus?.normal && d.requestedBolus?.amount) {
232+
d.requestedBolus.normal = d.requestedBolus.amount;
233+
delete d.requestedBolus.amount;
234+
}
235+
236+
// Use `amount` field instead of `normal` and/or `extended | duration` for recommendedBolus
237+
if (!d.recommendedBolus?.amount && (d.recommendedBolus?.extended || d.recommendedBolus?.normal)) {
238+
d.recommendedBolus.amount = (d.recommendedBolus.extended || 0) + (d.recommendedBolus.normal || 0);
239+
delete d.recommendedBolus.extended;
240+
delete d.recommendedBolus.normal;
241+
delete d.recommendedBolus.duration;
242+
}
237243
}
238244

239245
// We validate datums before converting the time and deviceTime to hammerTime integers,
@@ -326,28 +332,59 @@ export class DataUtil {
326332
if (d.type === 'bolus' && !!this.loopDataSetsByIdMap[d.uploadId]) {
327333
const timeThreshold = MS_IN_MIN;
328334

329-
const proximateDosingDecisions = _.filter(
335+
// Find the dosing decision that matches the bolus by checking if there is a definitive association
336+
d.dosingDecision = _.find(
330337
_.mapValues(this.bolusDosingDecisionDatumsByIdMap),
331-
({ time }) => {
332-
const timeOffset = Math.abs(time - d.time);
333-
return timeOffset <= timeThreshold;
334-
}
338+
({ associations = [] }) => _.some(associations, { reason: 'bolus', id: d.id })
335339
);
336340

337-
const sortedProximateDosingDecisions = _.orderBy(proximateDosingDecisions, ({ time }) => Math.abs(time - d.time), 'asc');
338-
const dosingDecisionWithMatchingNormal = _.find(sortedProximateDosingDecisions, dosingDecision => dosingDecision.requestedBolus?.normal === d.normal);
339-
d.dosingDecision = dosingDecisionWithMatchingNormal || sortedProximateDosingDecisions[0];
341+
// If no definitive dosing decision association is provided, such as can be the case with Tidepool
342+
// and DIY Loop, we look for the closest dosing decision within a time threshold
343+
if (!d.dosingDecision) {
344+
const proximateDosingDecisions = _.filter(
345+
_.mapValues(this.bolusDosingDecisionDatumsByIdMap),
346+
({ time, associations }) => {
347+
// If there is a definitive association, we skip this decision, as it would have been
348+
// associated with the bolus already in the code above if the id matched
349+
if (_.some(associations, { reason: 'bolus' })) return false;
350+
351+
const timeOffset = Math.abs(time - d.time);
352+
return timeOffset <= timeThreshold;
353+
}
354+
);
355+
356+
const sortedProximateDosingDecisions = _.orderBy(proximateDosingDecisions, ({ time }) => Math.abs(time - d.time), 'asc');
357+
const dosingDecisionWithMatchingNormal = _.find(sortedProximateDosingDecisions, dosingDecision => dosingDecision.requestedBolus?.normal === d.normal);
358+
359+
// Set the best-matching dosing decision, if available, or the first one within the time threshold
360+
d.dosingDecision = dosingDecisionWithMatchingNormal || sortedProximateDosingDecisions[0];
361+
362+
if (d.dosingDecision) {
363+
// Set the assocation to this bolus so that we don't risk associating it again if other proximate matches occur
364+
this.bolusDosingDecisionDatumsByIdMap[d.dosingDecision.id].associations = [
365+
...d.dosingDecision.associations || [],
366+
{ reason: 'bolus', id: d.id },
367+
];
368+
}
369+
}
340370

341371
if (d.dosingDecision) {
342372
// attach associated pump settings to dosingDecisions
343373
const associatedPumpSettingsId = _.find(d.dosingDecision.associations, { reason: 'pumpSettings' })?.id;
344374
d.dosingDecision.pumpSettings = this.pumpSettingsDatumsByIdMap[associatedPumpSettingsId];
345375

346376
// Translate relevant dosing decision data onto expected bolus fields
347-
d.expectedNormal = d.dosingDecision.requestedBolus?.normal;
348377
d.carbInput = d.dosingDecision.food?.nutrition?.carbohydrate?.net;
349378
d.bgInput = _.last(d.dosingDecision.bgHistorical || [])?.value;
350379
d.insulinOnBoard = d.dosingDecision.insulinOnBoard?.amount;
380+
381+
// Loop interrupted boluses may not have expectedNormal set,
382+
// so we set it to the requested normal from the dosing decision
383+
const requestedNormal = d.dosingDecision.requestedBolus?.normal;
384+
385+
if ((!d.expectedNormal && requestedNormal) && (d.normal !== requestedNormal)) {
386+
d.expectedNormal = requestedNormal;
387+
}
351388
}
352389
}
353390
};

src/utils/bolus.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,24 @@ export function getProgrammed(insulinEvent) {
119119
*/
120120
export function getRecommended(insulinEvent) {
121121
let event = insulinEvent;
122+
122123
if (_.get(insulinEvent, 'type') === 'bolus') {
123124
event = event.dosingDecision || getWizardFromInsulinEvent(insulinEvent);
124125
}
126+
125127
// a simple manual/"quick" bolus won't have a `recommended` field
126128
if (!event.recommendedBolus && !event.recommended) {
127129
return NaN;
128130
}
131+
129132
const netRecommendation = event.recommendedBolus
130-
? _.get(event, ['recommendedBolus', 'normal'], null)
133+
? _.get(event, ['recommendedBolus', 'amount'], null)
131134
: _.get(event, ['recommended', 'net'], null);
132135

133136
if (netRecommendation !== null) {
134137
return netRecommendation;
135138
}
139+
136140
let rec = 0;
137141
rec += _.get(event, ['recommended', 'carb'], 0);
138142
rec += _.get(event, ['recommended', 'correction'], 0);
@@ -369,7 +373,7 @@ export function isUnderride(insulinEvent) {
369373
export function isCorrection(insulinEvent) {
370374
const recommended = insulinEvent.dosingDecision
371375
? {
372-
correction: _.get(insulinEvent, 'dosingDecision.recommendedBolus.normal'),
376+
correction: _.get(insulinEvent, 'dosingDecision.recommendedBolus.amount'),
373377
carb: _.get(insulinEvent, 'dosingDecision.food.nutrition.carbohydrate.net', 0),
374378
}
375379
: _.get(insulinEvent, 'wizard.recommended', insulinEvent.recommended);
@@ -395,7 +399,7 @@ export function isAutomated(insulinEvent) {
395399
*/
396400
export function isOneButton(insulinEvent) {
397401
const bolus = getBolusFromInsulinEvent(insulinEvent);
398-
return _.get(bolus, 'deliveryContext') === 'oneButton';
402+
return _.get(bolus, 'deliveryContext') === 'oneButton' || _.get(bolus, 'dosingDecision.reason') === 'oneButtonBolus';
399403
}
400404

401405
/**

test/components/daily/BolusTooltip.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ const automated = {
3535
normal: 5,
3636
normalTime: '2017-11-11T05:45:52.000Z',
3737
subType: 'automated',
38+
tags: { automated: true },
3839
};
3940

4041
const oneButton = {
4142
normal: 5,
4243
normalTime: '2017-11-11T05:45:52.000Z',
4344
deliveryContext: 'oneButton',
45+
tags: { oneButton: true },
4446
};
4547

4648
const cancelled = {

test/utils/DataUtil.test.js

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,32 @@ describe('DataUtil', () => {
969969
});
970970

971971
context('dosingDecision', () => {
972+
it('should convert requestedBolus.amount to requestedBolus.normal and remove amount', () => {
973+
const datum = {
974+
type: 'dosingDecision',
975+
requestedBolus: { amount: 3.5 },
976+
};
977+
978+
dataUtil.validateDatumIn = sinon.stub().returns(true);
979+
dataUtil.normalizeDatumIn(datum);
980+
expect(datum.requestedBolus.normal).to.equal(3.5);
981+
expect(datum.requestedBolus).to.not.have.property('amount');
982+
});
983+
984+
it('should convert recommendedBolus.normal and recommendedBolus.extended to recommendedBolus.amount and remove normal, extended, duration', () => {
985+
const datum = {
986+
type: 'dosingDecision',
987+
recommendedBolus: { normal: 2, extended: 1, duration: 60000 },
988+
};
989+
990+
dataUtil.validateDatumIn = sinon.stub().returns(true);
991+
dataUtil.normalizeDatumIn(datum);
992+
expect(datum.recommendedBolus.amount).to.equal(3);
993+
expect(datum.recommendedBolus).to.not.have.property('normal');
994+
expect(datum.recommendedBolus).to.not.have.property('extended');
995+
expect(datum.recommendedBolus).to.not.have.property('duration');
996+
});
997+
972998
it('should add the datum to the `bolusDosingDecisionDatumsByIdMap`', () => {
973999
dataUtil.validateDatumIn = sinon.stub().returns(true);
9741000

@@ -1039,7 +1065,51 @@ describe('DataUtil', () => {
10391065
});
10401066

10411067
describe('joinBolusAndDosingDecision', () => {
1042-
it('should join loop dosing decisions, and associated pump settings, to boluses that are within a minute of each other', () => {
1068+
it('should join loop dosing decisions, and associated pump settings, to boluses that are definitively associated by ID', () => {
1069+
const uploadId = 'upload1';
1070+
const upload = { type: 'upload', id: uploadId, dataSetType: 'continuous', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } };
1071+
const bolus = { type: 'bolus', id: 'bolus1', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), origin: { name: 'org.tidepool.Loop' } };
1072+
const bolus2 = { type: 'bolus', id: 'bolus2', uploadId, time: Date.parse('2024-02-02T11:05:59.000Z'), origin: { name: 'org.tidepool.Loop' } };
1073+
const pumpSettings = { ...loopMultirate, id: 'pumpSettings1' };
1074+
1075+
const dosingDecision = {
1076+
type: 'dosingDecision',
1077+
id: 'dosingDecision1',
1078+
time: Date.parse('2024-02-02T10:05:00.000Z'),
1079+
origin: { name: 'org.tidepool.Loop' },
1080+
associations: [
1081+
{ reason: 'bolus', id: 'bolus2' },
1082+
{ reason: 'pumpSettings', id: 'pumpSettings1' },
1083+
],
1084+
requestedBolus: { normal: 12 },
1085+
insulinOnBoard: { amount: 4 },
1086+
food: { nutrition: { carbohydrate: { net: 30 } } },
1087+
bgHistorical: [
1088+
{ value: 100 },
1089+
{ value: 110 },
1090+
],
1091+
};
1092+
1093+
dataUtil.bolusDosingDecisionDatumsByIdMap = { dosingDecision1: dosingDecision };
1094+
dataUtil.pumpSettingsDatumsByIdMap = { pumpSettings1: pumpSettings };
1095+
dataUtil.loopDataSetsByIdMap = { [uploadId]: upload };
1096+
1097+
_.each([bolus, bolus2], dataUtil.joinBolusAndDosingDecision);
1098+
// should not attach dosing decision to bolus that is not associated
1099+
expect(bolus.dosingDecision).to.be.undefined;
1100+
1101+
// should attach associated pump settings to dosingDecisions
1102+
expect(bolus2.dosingDecision).to.eql(dosingDecision);
1103+
expect(bolus2.dosingDecision.pumpSettings).to.eql(pumpSettings);
1104+
1105+
// should translate relevant dosing decision data onto expected bolus fields
1106+
expect(bolus2.expectedNormal).to.equal(12);
1107+
expect(bolus2.carbInput).to.equal(30);
1108+
expect(bolus2.bgInput).to.equal(110);
1109+
expect(bolus2.insulinOnBoard).to.equal(4);
1110+
});
1111+
1112+
it('should join loop dosing decisions, and associated pump settings, to boluses that are within a minute of each other if not definitive associations exist', () => {
10431113
const uploadId = 'upload1';
10441114
const upload = { type: 'upload', id: uploadId, dataSetType: 'continuous', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } };
10451115
const bolus = { type: 'bolus', id: 'bolus1', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), origin: { name: 'org.tidepool.Loop' } };
@@ -1076,6 +1146,38 @@ describe('DataUtil', () => {
10761146
expect(bolus.insulinOnBoard).to.equal(4);
10771147
});
10781148

1149+
it('should not add expectedNormal to joined loop dosing decisions if the requested normal is equal to the bolus normal', () => {
1150+
const uploadId = 'upload1';
1151+
const upload = { type: 'upload', id: uploadId, dataSetType: 'continuous', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } };
1152+
const bolus = { type: 'bolus', id: 'bolus1', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), origin: { name: 'org.tidepool.Loop' }, normal: 12 };
1153+
const pumpSettings = { ...loopMultirate, id: 'pumpSettings1' };
1154+
1155+
const dosingDecision = {
1156+
type: 'dosingDecision',
1157+
id: 'dosingDecision1',
1158+
time: Date.parse('2024-02-02T10:05:00.000Z'),
1159+
origin: { name: 'org.tidepool.Loop' },
1160+
associations: [{ reason: 'pumpSettings', id: 'pumpSettings1' }],
1161+
requestedBolus: { normal: 12 },
1162+
insulinOnBoard: { amount: 4 },
1163+
food: { nutrition: { carbohydrate: { net: 30 } } },
1164+
bgHistorical: [
1165+
{ value: 100 },
1166+
{ value: 110 },
1167+
],
1168+
};
1169+
1170+
dataUtil.bolusDosingDecisionDatumsByIdMap = { dosingDecision1: dosingDecision };
1171+
dataUtil.pumpSettingsDatumsByIdMap = { pumpSettings1: pumpSettings };
1172+
dataUtil.loopDataSetsByIdMap = { [uploadId]: upload };
1173+
1174+
dataUtil.joinBolusAndDosingDecision(bolus);
1175+
expect(bolus.dosingDecision).to.eql(dosingDecision);
1176+
1177+
// should not add the bolus.expectedNormal field since the requested normal is equal to the bolus normal
1178+
expect(bolus.expectedNormal).to.be.undefined;
1179+
});
1180+
10791181
it('should not join loop dosing decisions to boluses that are outside of a minute of each other', () => {
10801182
const dosingDecision = { type: 'dosingDecision', id: 'dosingDecision1', time: Date.parse('2024-02-02T10:05:00.000Z'), origin: { name: 'org.tidepool.Loop' } };
10811183
const bolus = { type: 'bolus', id: 'bolus1', time: Date.parse('2024-02-02T10:06:01.000Z'), origin: { name: 'org.tidepool.Loop' } };

test/utils/bolus.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ const withDosingDecision = {
303303
insulinOnBoard: 2.654,
304304
dosingDecision: {
305305
recommendedBolus: {
306-
normal: 2.8,
306+
amount: 2.8,
307307
},
308308
insulinOnBoard: {
309309
amount: 2.2354,
@@ -321,7 +321,7 @@ const withDosingDecisionOverride = {
321321
dosingDecision: {
322322
...withDosingDecision.dosingDecision,
323323
recommendedBolus: {
324-
normal: withDosingDecision.normal - 0.5,
324+
amount: withDosingDecision.normal - 0.5,
325325
},
326326
},
327327
};
@@ -331,7 +331,7 @@ const withDosingDecisionUnderride = {
331331
dosingDecision: {
332332
...withDosingDecision.dosingDecision,
333333
recommendedBolus: {
334-
normal: withDosingDecision.normal + 0.5,
334+
amount: withDosingDecision.normal + 0.5,
335335
},
336336
},
337337
};
@@ -340,7 +340,7 @@ const withDosingDecisionCorrection = {
340340
...withDosingDecision,
341341
dosingDecision: {
342342
...withDosingDecision.dosingDecision,
343-
recommendedBolus: { normal: 0.5 },
343+
recommendedBolus: { amount: 0.5 },
344344
food: { nutrition: { carbohydrate: { net: 0 } } },
345345
},
346346
};
@@ -552,9 +552,9 @@ describe('bolus utilities', () => {
552552
expect(bolusUtils.getRecommended(withNetRec)).to.equal(net);
553553
});
554554

555-
it('should return `normal` rec when `normal` rec exists on dosing decision', () => {
555+
it('should return `amount` rec when `amount` rec exists on dosing decision', () => {
556556
const { recommendedBolus } = withDosingDecision.dosingDecision;
557-
expect(bolusUtils.getRecommended(withDosingDecision)).to.equal(recommendedBolus.normal);
557+
expect(bolusUtils.getRecommended(withDosingDecision)).to.equal(recommendedBolus.amount);
558558
});
559559

560560
it('should return 0 when no bolus recommended, even if overridden', () => {

0 commit comments

Comments
 (0)