Skip to content

Commit

Permalink
Merge pull request #1497 from tidepool-org/WEB-3346-summary-accuracy
Browse files Browse the repository at this point in the history
[WEB-3346, WEB-3351] - CGM Summary Enhancements
  • Loading branch information
henry-tp authored Jan 23, 2025
2 parents fc24c6b + 13229d7 commit 9e6bd80
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 369 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import moment from 'moment';
import { MS_IN_MIN } from '../../../../core/constants';
import isNumber from 'lodash/isNumber';
import { utils as vizUtils } from '@tidepool/viz';
const { getOffset, formatDateRange } = vizUtils.datetime;

const getDateRange = (startDate, endDate, dateParseFormat, _prefix, monthFormat, timezone) => {
let start = startDate;
let end = endDate;

if (isNumber(startDate) && isNumber(endDate)) {
start = startDate - getOffset(startDate, timezone) * MS_IN_MIN;
end = endDate - getOffset(endDate, timezone) * MS_IN_MIN;
}

return formatDateRange(start, end, dateParseFormat, monthFormat);
};

const getReportDaysText = (newestDatum, oldestDatum, bgDaysWorn, timezone) => {
const reportDaysText = bgDaysWorn === 1
? moment.utc(newestDatum?.time - getOffset(newestDatum?.time, timezone) * MS_IN_MIN).format('MMMM D, YYYY')
: getDateRange(oldestDatum?.time, newestDatum?.time, undefined, '', 'MMMM', timezone);

return reportDaysText;
};

export default getReportDaysText;
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import React from 'react';
import colorPalette from '../../../themes/colorPalette';
import colorPalette from '../../../../themes/colorPalette';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Flex, Box, Text } from 'theme-ui';
import moment from 'moment';
import { utils as vizUtils } from '@tidepool/viz';
import utils from '../../../core/utils';
import { MGDL_UNITS } from '../../../core/constants';

const formatDateRange = (startEndpoint, endEndpoint, timezoneName) => {
const startDate = moment.utc(startEndpoint).tz(timezoneName);
const endDate = moment.utc(endEndpoint).tz(timezoneName);
const startYear = startDate.year();
const endYear = endDate.year();

if (startYear !== endYear) {
return `${startDate.format('MMMM D, YYYY')} - ${endDate.format('MMMM D, YYYY')}`;
}

return `${startDate.format('MMMM D')} - ${endDate.format('MMMM D')}, ${endDate.format('YYYY')}`;
}
const { formatDatum, bankersRound } = vizUtils.stat;
const { getTimezoneFromTimePrefs } = vizUtils.datetime;
import { MGDL_UNITS } from '../../../../core/constants';
import getReportDaysText from './getReportDaysText';

const TableRow = ({ label, sublabel, value, units, id }) => {
return (
Expand All @@ -46,8 +33,8 @@ const TableRow = ({ label, sublabel, value, units, id }) => {
{units && <Text sx={{ fontWeight: 'medium', fontSize: 0 }}>{units}</Text>}
</Box>
</Flex>
)
}
);
};

const CGMStatistics = ({ agpCGM }) => {
const { t } = useTranslation();
Expand All @@ -56,40 +43,39 @@ const CGMStatistics = ({ agpCGM }) => {

const {
timePrefs,
bgPrefs: { bgUnits },
bgPrefs,
data: {
current: {
endpoints: { days: endpointDays },
stats: {
bgExtents: { newestDatum, oldestDatum },
bgExtents: { newestDatum, oldestDatum, bgDaysWorn },
sensorUsage: { sensorUsageAGP },
averageGlucose: { averageGlucose },
glucoseManagementIndicator: { glucoseManagementIndicatorAGP },
coefficientOfVariation: { coefficientOfVariation }
coefficientOfVariation: { coefficientOfVariation },
},
}
}
},
},
} = agpCGM;

const timezoneName = vizUtils.datetime.getTimezoneFromTimePrefs(timePrefs);
const { bgUnits } = bgPrefs;

const avgGlucosePrecision = bgUnits === MGDL_UNITS ? 0 : 1;
const avgGlucoseTarget = bgUnits === MGDL_UNITS ? '154' : '8.6';
const timezone = getTimezoneFromTimePrefs(timePrefs);

const dateRange = formatDateRange(oldestDatum.time, newestDatum.time, timezoneName);
const daySpan = endpointDays;
const cgmActive = utils.roundToPrecision(sensorUsageAGP, 1);
const avgGlucose = utils.roundToPrecision(averageGlucose, avgGlucosePrecision);
const gmi = utils.roundToPrecision(glucoseManagementIndicatorAGP, 1);
const cov = utils.roundToPrecision(coefficientOfVariation, 1);
const avgGlucoseTarget = bgUnits === MGDL_UNITS ? '154' : '8.6';

const dateRange = getReportDaysText(newestDatum, oldestDatum, bgDaysWorn, timezone);
const cgmActive = bankersRound(sensorUsageAGP, 1);
const avgGlucose = formatDatum({ value: averageGlucose }, 'bgValue', { bgPrefs, useAGPFormat: true });
const gmi = formatDatum({ value: glucoseManagementIndicatorAGP }, 'gmi', { bgPrefs, useAGPFormat: true });
const cov = formatDatum({ value: coefficientOfVariation }, 'cv', { bgPrefs, useAGPFormat: true });

return (
<Flex sx={{ alignItems: 'center', width: '100%', height: '100%' }} id='agp-cgm-statistics'>
<Box sx={{ width: '100%' }}>
<TableRow
id="agp-table-time-range"
label={t('Time Range')}
value={t('{{dateRange}} ({{daySpan}} days)', { dateRange, daySpan })}
value={t('{{dateRange}} ({{bgDaysWorn}} days)', { dateRange, bgDaysWorn })}
/>
<TableRow
id="agp-table-cgm-active"
Expand All @@ -101,26 +87,26 @@ const CGMStatistics = ({ agpCGM }) => {
id="agp-table-avg-glucose"
label={t('Average Glucose')}
sublabel={t('(Goal <{{avgGlucoseTarget}} {{bgUnits}})', { avgGlucoseTarget, bgUnits })}
value={`${avgGlucose}`}
value={avgGlucose?.value}
units={` ${bgUnits}`}
/>
<TableRow
id="agp-table-gmi"
label={t('Glucose Management Indicator')}
sublabel={t('(Goal <7%)')}
value={`${gmi}`}
units="%"
value={gmi?.value}
units={gmi?.suffix}
/>
<TableRow
id="agp-table-cov"
label={t('Glucose Variability')}
sublabel={t('(Defined as a percent coefficient of variation. Goal <= 36%)')}
value={`${cov}`}
units="%"
value={cov?.value}
units={cov?.suffix}
/>
</Box>
</Flex>
)
}
);
};

export default CGMStatistics;
3 changes: 2 additions & 1 deletion app/pages/dashboard/PatientDrawer/Content.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ const Content = ({ api, patientId, agpPeriodInDays }) => {

if (status === STATUS.NO_PATIENT_DATA) return <NoPatientData patientName={patient?.fullName}/>;
if (status === STATUS.INSUFFICIENT_DATA) return <InsufficientData />;
if (status !== STATUS.SVGS_GENERATED) return <Loader show={true} overlay={false} />

if (status !== STATUS.SVGS_GENERATED) return <Loader show={true} overlay={false} />;

const percentInRanges = svgDataURLS?.agpCGM?.percentInRanges;
const ambulatoryGlucoseProfile = svgDataURLS?.agpCGM?.ambulatoryGlucoseProfile;
Expand Down
101 changes: 16 additions & 85 deletions app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,50 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../../../../components/elements/Button';
import utils from '../../../../core/utils';
import { MGDL_UNITS } from '../../../../core/constants';
import { MS_IN_HOUR } from '../../../../core/constants';
import { Box } from 'theme-ui';
import { utils as vizUtils } from '@tidepool/viz';
const { TextUtil } = vizUtils.text;
import { Box } from 'theme-ui'
import moment from 'moment';

const formatDateRange = (startEndpoint, endEndpoint, timezoneName) => {
const startDate = moment.utc(startEndpoint).tz(timezoneName);
const endDate = moment.utc(endEndpoint).tz(timezoneName);
const startYear = startDate.year();
const endYear = endDate.year();

if (startYear !== endYear) {
return `${startDate.format('MMMM D, YYYY')} - ${endDate.format('MMMM D, YYYY')}`;
}

return `${startDate.format('MMMM D')} - ${endDate.format('MMMM D')}, ${endDate.format('YYYY')}`;
}

const getCGMClipboardText = (patient, agpCGM, t) => {
if (!agpCGM || !patient) return '';

const { fullName, birthDate } = patient;

const {
timePrefs,
bgPrefs: { bgUnits },
data: {
current: {
stats: {
bgExtents: { newestDatum, oldestDatum },
averageGlucose: { averageGlucose },
timeInRange: { counts },
},
}
}
} = agpCGM;

const timezoneName = vizUtils.datetime.getTimezoneFromTimePrefs(timePrefs);

const currentDate = moment().format('MMMM Do, YYYY');

// TODO: Add test for no data scenario
const dateRange = formatDateRange(oldestDatum?.time, newestDatum?.time, timezoneName);

const targetRange = bgUnits === MGDL_UNITS ? '70-180' : '3.9-10.0';
const lowRange = bgUnits === MGDL_UNITS ? '54-70' : '3.0-3.9';
const veryLowRange = bgUnits === MGDL_UNITS ? '<54' : '<3.0';

const countsInTarget = utils.roundToPrecision((counts.target / counts.total) * 100, 0);
const countsInLow = utils.roundToPrecision((counts.low * 100 ) / counts.total, 0);
const countsInVeryLow = utils.roundToPrecision((counts.veryLow * 100 ) / counts.total, 0);

const avgGlucose = utils.roundToPrecision(averageGlucose, bgUnits === MGDL_UNITS ? 0 : 1);

const textUtil = new TextUtil();
let clipboardText = '';

clipboardText += textUtil.buildTextLine(fullName);
clipboardText += textUtil.buildTextLine(t('Date of birth: {{birthDate}}', { birthDate }));
clipboardText += textUtil.buildTextLine(t('Exported from Tidepool TIDE: {{currentDate}}', { currentDate }));
clipboardText += textUtil.buildTextLine('');
clipboardText += textUtil.buildTextLine(t('Reporting Period: {{dateRange}}', { dateRange }));
clipboardText += textUtil.buildTextLine('');
clipboardText += textUtil.buildTextLine(t('Avg. Daily Time In Range ({{bgUnits}})', { bgUnits }));
clipboardText += textUtil.buildTextLine(t('{{targetRange}} {{countsInTarget}}%', { targetRange, countsInTarget }));
clipboardText += textUtil.buildTextLine(t('{{lowRange}} {{countsInLow}}%', { lowRange, countsInLow }));
clipboardText += textUtil.buildTextLine(t('{{veryLowRange}} {{countsInVeryLow}}%', { veryLowRange, countsInVeryLow }));
clipboardText += textUtil.buildTextLine('');
clipboardText += textUtil.buildTextLine(t('Avg. Glucose (CGM): {{avgGlucose}} {{bgUnits}}', { avgGlucose, bgUnits }));

return clipboardText;
}
const { agpCGMText } = vizUtils.text;

const STATE = {
DEFAULT: 'DEFAULT',
CLICKED: 'CLICKED',
}
};

const CGMClipboardButton = ({ patient, agpCGM }) => {
const CGMClipboardButton = ({ patient, data }) => {
const { t } = useTranslation();
const [buttonState, setButtonState] = useState(STATE.DEFAULT);
const clipboardText = useMemo(() => agpCGMText(patient, data), [patient, data]);

useEffect(() => {
let buttonTextEffect = setTimeout(() => {
setButtonState(STATE.DEFAULT)
setButtonState(STATE.DEFAULT);
}, 1000);

return () => {
clearTimeout(buttonTextEffect);
}
}, [buttonState])
};
}, [buttonState]);

const sensorUsage = agpCGM?.data?.current?.stats?.sensorUsage?.sensorUsage || 0;
const { count, sampleFrequency } = data?.data?.current?.stats?.sensorUsage || {};

const isDisabled = !agpCGM || sensorUsage < 86400000; // minimum 24 hours
const hoursOfCGMData = (count * sampleFrequency) / MS_IN_HOUR;

const clipboardText = useMemo(() => getCGMClipboardText(patient, agpCGM, t), [patient, agpCGM, t]);
const isDataInsufficient = !hoursOfCGMData || hoursOfCGMData < 24;

const handleCopy = () => {
navigator?.clipboard?.writeText(clipboardText);
setButtonState(STATE.CLICKED);
}
};

return (
<Button disabled={isDisabled} onClick={handleCopy} variant="secondary">
<Button disabled={isDataInsufficient} onClick={handleCopy} variant="secondary">
{buttonState === STATE.CLICKED
? <Box>{t('Copied ✓')}</Box>
: <Box>{t('Copy as Text')}</Box>
}
</Button>
)
}
);
};

export default CGMClipboardButton;
12 changes: 6 additions & 6 deletions app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {

const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const patient = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]?.patients?.[patientId]);
const agpCGM = useSelector(state => state.blip.pdf?.data?.agpCGM); // IMPORTANT: Data taken from Redux PDF slice
const pdf = useSelector(state => state.blip.pdf); // IMPORTANT: Data taken from Redux PDF slice

useEffect(() => {
// DOB field in Patient object may not be populated in TIDE Dashboard, so we need to refetch
Expand All @@ -35,10 +35,10 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
const handleReviewSuccess = () => {
setTimeout(() => {
onClose();
}, 500)
}
}, 500);
};

const { fullName, birthDate } = patient || {};
const { fullName, birthDate } = patient || {};

return (
<Box sx={{ display: 'grid', gridTemplateColumns: '32fr 18fr 18fr 32fr', gap: 3, minHeight: '42px', marginBottom: 3 }}>
Expand All @@ -47,7 +47,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
{fullName}
</Text>
{ birthDate &&
<Text sx={{ color: colorPalette.extended.grays[10], fontWeight: 'medium', fontSize: 0 }}>
<Text sx={{ color: colorPalette.extended.grays[5], fontWeight: 'medium', fontSize: 0 }}>
{t('DOB: {{birthDate}}', { birthDate })}
</Text>
}
Expand All @@ -60,7 +60,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
</Flex>

<Flex sx={{ justifyContent: 'flex-start', alignItems: 'center' }}>
<CGMClipboardButton patient={patient} agpCGM={agpCGM} />
<CGMClipboardButton patient={patient} data={pdf?.data?.agpCGM} />
</Flex>

<Flex sx={{ fontSize: 0, alignItems: 'center', justifyContent: 'flex-end' }}>
Expand Down
Loading

0 comments on commit 9e6bd80

Please sign in to comment.