diff --git a/.gitignore b/.gitignore index 6328e23ba..e14cec456 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ package-lock.json !.yarn/releases !.yarn/sdks !.yarn/versions + +.codegpt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index d44cac1df..ef1cc1d11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,9 @@ -sudo: false - -language: node_js - +os: linux dist: jammy +language: node_js node_js: - "20.8.0" - - "stable" cache: yarn: true @@ -31,7 +28,3 @@ services: script: - npm test - ./artifact.sh - -matrix: - allow_failures: - - node_js: "stable" diff --git a/data/print/fixtures.js b/data/print/fixtures.js index 427a9d4e2..efcef8c38 100644 --- a/data/print/fixtures.js +++ b/data/print/fixtures.js @@ -52,7 +52,6 @@ export const basicsData = { timePrefs, bgPrefs, metaData, - query: { excludeDaysWithoutBolus: true }, data: { current: { endpoints: { diff --git a/data/types.js b/data/types.js index ba6b393f6..9c1be95d0 100644 --- a/data/types.js +++ b/data/types.js @@ -148,6 +148,7 @@ export class CBG extends Common { this.deviceTime = opts.deviceTime; this.deviceId = opts.deviceId; + this.origin = opts.origin; this.units = opts.units; this.value = opts.value; @@ -340,6 +341,8 @@ export class Upload extends Common { this.deviceManufacturers = opts.deviceManufacturers; this.deviceSerialNumber = opts.deviceSerialNumber; this.dataSetType = opts.dataSetType; + this.origin = opts.origin; + this.client = opts.client; this.time = this.makeTime(); this.timezone = opts.timezone; diff --git a/package.json b/package.json index 6e34ca2fb..2441eebb6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "node": "20.8.0" }, "packageManager": "yarn@3.6.4", - "version": "1.44.0", + "version": "1.45.0-rc.8", "description": "Tidepool data visualization for diabetes device data.", "keywords": [ "data visualization" @@ -88,8 +88,8 @@ "text-table": "0.2.0", "translate-svg-path": "0.0.1", "util": "0.12.5", - "victory": "31.3.0", - "victory-core": "31.2.0", + "victory": "37.3.5", + "victory-core": "37.3.5", "voilab-pdf-table": "0.5.1" }, "devDependencies": { @@ -166,7 +166,7 @@ "babel-core": "6.x || ^7.0.0-bridge.0", "classnames": "2.x", "react": "16.x", - "react-addons-update": "16.x", + "react-addons-update": "15.6.x", "react-dom": "16.x", "react-redux": "8.x", "redux": "4.x" diff --git a/src/components/common/stat/BgBar.js b/src/components/common/stat/BgBar.js index 79f150048..406110dd3 100644 --- a/src/components/common/stat/BgBar.js +++ b/src/components/common/stat/BgBar.js @@ -32,7 +32,7 @@ export const BgBar = props => { const widths = { low: scale.y(bgBounds.targetLowerBound) * widthCorrection, target: scale.y(bgBounds.targetUpperBound - bgBounds.targetLowerBound) * widthCorrection, - high: scale.y(domain.x[1] - bgBounds.targetUpperBound) * widthCorrection, + high: scale.y(domain.y[1] - bgBounds.targetUpperBound) * widthCorrection, }; const barRadius = barWidth / 2; diff --git a/src/components/common/stat/BgBarLabel.js b/src/components/common/stat/BgBarLabel.js index 38a8d9a1d..8b00968f7 100644 --- a/src/components/common/stat/BgBarLabel.js +++ b/src/components/common/stat/BgBarLabel.js @@ -26,7 +26,8 @@ export const BgBarLabel = props => { textAnchor="end" verticalAnchor="middle" dy={-(barWidth / 2 - 1)} - x={scale.y(domain.x[1])} + x={scale.y(domain.y[1])} + dx={0} /> ); diff --git a/src/components/common/stat/HoverBar.js b/src/components/common/stat/HoverBar.js index f3a47fb0c..5667681be 100644 --- a/src/components/common/stat/HoverBar.js +++ b/src/components/common/stat/HoverBar.js @@ -18,7 +18,7 @@ export const HoverBar = props => { y: _.noop, }, width, - y, + x } = props; const barGridWidth = barWidth / 6; @@ -34,12 +34,13 @@ export const HoverBar = props => { y={scale.x(index + 1) - (barWidth / 2) - (barSpacing / 2)} rx={barGridRadius} ry={barGridRadius} - width={scale.y(domain.x[1])} + width={scale.y(domain.y[1])} height={barWidth + barSpacing} style={{ stroke: 'transparent', fill: 'transparent', }} + {...props.events} /> @@ -49,7 +50,7 @@ export const HoverBar = props => { y={scale.x(index + 1) - (barGridWidth / 2)} rx={barGridRadius} ry={barGridRadius} - width={scale.y(domain.x[1]) - chartLabelWidth} + width={scale.y(domain.y[1]) - chartLabelWidth} height={barGridWidth} style={{ stroke: 'transparent', @@ -61,7 +62,7 @@ export const HoverBar = props => { diff --git a/src/components/common/stat/HoverBarLabel.js b/src/components/common/stat/HoverBarLabel.js index 3a1b2acc3..25f6c3d88 100644 --- a/src/components/common/stat/HoverBarLabel.js +++ b/src/components/common/stat/HoverBarLabel.js @@ -20,6 +20,12 @@ export const HoverBarLabel = props => { style = {}, text, tooltipText, + // Victory animate sometimes passes undefined to the y prop which errors out the label rendering + // but eventually settles on the correct value for the final render, but we default to 15 (the lowest + // common observed value) to avoid the error + // There's a lot of strange behavior with animate and it's being completely rewritten + // see: https://github.com/FormidableLabs/victory/issues/2104 + y = 15, } = props; const tooltipFontSize = _.min([barWidth / 2, 12]); @@ -63,7 +69,8 @@ export const HoverBarLabel = props => { style={labelStyle} textAnchor="end" verticalAnchor="middle" - x={scale.y(domain.x[1])} + x={scale.y(domain.y[1])} + y={y} dx={-(labelUnitsTextSize.width * 1.9)} /> { style={labelUnitsStyle} textAnchor="end" verticalAnchor="middle" - x={scale.y(domain.x[1])} + x={scale.y(domain.y[1])} + y={y} dx={0} /> {tooltipTextSize.width > 0 && ( @@ -81,15 +89,17 @@ export const HoverBarLabel = props => { {...props} cornerRadius={tooltipRadius} datum={tooltipDatum} - x={scale.y(domain.x[1]) - style.paddingLeft - tooltipTextSize.width - (tooltipRadius * 2)} + x={scale.y(domain.y[1]) - style.paddingLeft - tooltipTextSize.width - (tooltipRadius * 2)} + y={y} + dx={0} flyoutStyle={{ display: disabled ? 'none' : 'inherit', stroke: colors.axis, strokeWidth: 2, fill: colors.white, }} - width={tooltipTextSize.width + (tooltipRadius * 2)} - height={tooltipHeight} + flyoutWidth={tooltipTextSize.width + (tooltipRadius * 2)} + flyoutHeight={tooltipHeight} pointerLength={0} pointerWidth={0} renderInPortal={false} diff --git a/src/components/common/stat/Stat.js b/src/components/common/stat/Stat.js index 8ec791bb4..e94ef4f13 100644 --- a/src/components/common/stat/Stat.js +++ b/src/components/common/stat/Stat.js @@ -19,9 +19,9 @@ import BgBar from './BgBar'; import BgBarLabel from './BgBarLabel'; import StatTooltip from '../tooltips/StatTooltip'; import StatLegend from './StatLegend'; -import CollapseIconOpen from './assets/expand-more-24-px.svg'; -import CollapseIconClose from './assets/chevron-right-24-px.svg'; -import InfoIcon from './assets/info-outline-24-px.svg'; +import CollapseIconOpen from './assets/expand-more-24-px.png'; +import CollapseIconClose from './assets/chevron-right-24-px.png'; +import InfoIcon from './assets/info-outline-24-px.png'; import InputGroup from '../controls/InputGroup'; /* global document */ @@ -465,8 +465,8 @@ class Stat extends PureComponent { height = chartProps.height || barWidth * 6; domain = { - x: [0, bgUnits === MGDL_UNITS ? MGDL_CLAMP_TOP : MMOLL_CLAMP_TOP], - y: [0, 1], + x: [0, 1], + y: [0, bgUnits === MGDL_UNITS ? MGDL_CLAMP_TOP : MMOLL_CLAMP_TOP], }; padding = { @@ -476,7 +476,7 @@ class Stat extends PureComponent { _.assign(chartProps, { alignment: 'middle', - containerComponent: , + containerComponent: , cornerRadius: { topLeft: 2, bottomLeft: 2, topRight: 2, bottomRight: 2 }, data: _.map(chartData, (d, i) => ({ ...d, @@ -502,7 +502,7 @@ class Stat extends PureComponent { barWidth={barWidth} bgPrefs={props.bgPrefs} domain={domain} - text={(datum = {}) => { + text={({ datum = {} }) => { const datumRef = _.get(chartData, datum.index, datum); const { value } = formatDatum( _.get(datumRef, 'deviation', datumRef), @@ -511,7 +511,7 @@ class Stat extends PureComponent { ); return `${value}`; }} - tooltipText={(datum = {}) => { + tooltipText={({ datum = {} }) => { const { value, suffix } = formatDatum( _.get(chartData, datum.index, datum), props.dataFormat.tooltip, @@ -525,11 +525,11 @@ class Stat extends PureComponent { renderer: VictoryBar, style: { data: { - fill: datum => this.getDatumColor(datum), + fill: ({ datum }) => this.getDatumColor(datum), width: () => barWidth, }, labels: { - fill: datum => this.getDatumColor(_.assign({}, datum, formatDatum( + fill: ({ datum }) => this.getDatumColor(_.assign({}, datum, formatDatum( datum, props.dataFormat.label, props @@ -557,8 +557,8 @@ class Stat extends PureComponent { } domain = { - x: [0, 1], - y: [0, chartData.length], + x: [0, chartData.length], + y: [0, 1], }; padding = { @@ -568,7 +568,7 @@ class Stat extends PureComponent { _.assign(chartProps, { alignment: 'middle', - containerComponent: , + containerComponent: , cornerRadius: { topLeft: 2, bottomLeft: 2, topRight: 2, bottomRight: 2 }, data: _.map(chartData, (d, i) => ({ ...d, @@ -647,11 +647,11 @@ class Stat extends PureComponent { renderer: VictoryBar, style: { data: { - fill: datum => (datum._y === 0 ? 'transparent' : this.getDatumColor(datum)), + fill: ({ datum }) => (datum._y === 0 ? 'transparent' : this.getDatumColor(datum)), width: () => barWidth, }, labels: { - fill: datum => this.getDatumColor(_.assign({}, datum, formatDatum( + fill: ({ datum }) => this.getDatumColor(_.assign({}, datum, formatDatum( datum, props.dataFormat.label, props diff --git a/src/components/common/stat/assets/chevron-right-24-px.png b/src/components/common/stat/assets/chevron-right-24-px.png new file mode 100644 index 000000000..f12d70c6e Binary files /dev/null and b/src/components/common/stat/assets/chevron-right-24-px.png differ diff --git a/src/components/common/stat/assets/chevron-right-24-px.svg b/src/components/common/stat/assets/chevron-right-24-px.svg deleted file mode 100644 index a13dfa428..000000000 --- a/src/components/common/stat/assets/chevron-right-24-px.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/components/common/stat/assets/expand-more-24-px.png b/src/components/common/stat/assets/expand-more-24-px.png new file mode 100644 index 000000000..e61dcca6d Binary files /dev/null and b/src/components/common/stat/assets/expand-more-24-px.png differ diff --git a/src/components/common/stat/assets/expand-more-24-px.svg b/src/components/common/stat/assets/expand-more-24-px.svg deleted file mode 100644 index 397517334..000000000 --- a/src/components/common/stat/assets/expand-more-24-px.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/components/common/stat/assets/info-outline-24-px.png b/src/components/common/stat/assets/info-outline-24-px.png new file mode 100644 index 000000000..76c564e79 Binary files /dev/null and b/src/components/common/stat/assets/info-outline-24-px.png differ diff --git a/src/components/common/stat/assets/info-outline-24-px.svg b/src/components/common/stat/assets/info-outline-24-px.svg deleted file mode 100644 index b593ddeb4..000000000 --- a/src/components/common/stat/assets/info-outline-24-px.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/components/daily/bolustooltip/BolusTooltip.js b/src/components/daily/bolustooltip/BolusTooltip.js index 3da18f759..55f6141a6 100644 --- a/src/components/daily/bolustooltip/BolusTooltip.js +++ b/src/components/daily/bolustooltip/BolusTooltip.js @@ -237,8 +237,8 @@ class BolusTooltip extends PureComponent { if (this.isLoop) { const { activeSchedule, carbRatios, insulinSensitivities } = _.get(wizard, 'dosingDecision.pumpSettings', {}); - carbRatio = _.findLast(_.sortBy(carbRatios?.[activeSchedule] || [], 'start'), ({ start }) => start < this.msPer24)?.amount || null; - isf = _.findLast(_.sortBy(insulinSensitivities?.[activeSchedule] || [], 'start'), ({ start }) => start < this.msPer24)?.amount || null; + carbRatio = _.findLast(_.sortBy(carbRatios?.[activeSchedule] || [], 'start'), ({ start }) => start < this.msPer24)?.amount || carbRatio; + isf = _.findLast(_.sortBy(insulinSensitivities?.[activeSchedule] || [], 'start'), ({ start }) => start < this.msPer24)?.amount || isf; } const delivered = bolusUtils.getDelivered(wizard); @@ -284,7 +284,7 @@ class BolusTooltip extends PureComponent { ); const bgLine = !!bg && (
-
{t('BG')} ({this.bgUnits})
+
{t('Glucose')} ({this.bgUnits})
{this.formatBgValue(bg)}
diff --git a/src/components/daily/cbgtooltip/CBGTooltip.js b/src/components/daily/cbgtooltip/CBGTooltip.js index 0b1571945..317db90a9 100644 --- a/src/components/daily/cbgtooltip/CBGTooltip.js +++ b/src/components/daily/cbgtooltip/CBGTooltip.js @@ -18,6 +18,7 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import _ from 'lodash'; +import i18next from 'i18next'; import { classifyBgValue, reshapeBgClassesToBgBounds, @@ -30,13 +31,20 @@ import Tooltip from '../../common/tooltips/Tooltip'; import colors from '../../../styles/colors.css'; import styles from './CBGTooltip.css'; +const t = i18next.t.bind(i18next); + class CBGTooltip extends PureComponent { + constructor(props) { + super(props); + this.bgUnits = this.props.bgPrefs?.bgUnits || ''; + } + renderCBG() { const cbg = this.props.cbg; const outOfRangeMessage = getOutOfRangeAnnotationMessage(cbg); const rows = [
-
BG
+
{t('Glucose')} ({this.bgUnits})
{`${formatBgValue(cbg.value, this.props.bgPrefs, getOutOfRangeThreshold(cbg))}`}
diff --git a/src/components/settings/NonTandem.js b/src/components/settings/NonTandem.js index 133afc94b..ddef1f3bc 100644 --- a/src/components/settings/NonTandem.js +++ b/src/components/settings/NonTandem.js @@ -295,7 +295,7 @@ const NonTandem = (props) => { NonTandem.propTypes = { bgUnits: PropTypes.oneOf([MMOLL_UNITS, MGDL_UNITS]).isRequired, copySettingsClicked: PropTypes.func.isRequired, - deviceKey: PropTypes.oneOf(['animas', 'carelink', 'insulet', 'medtronic', 'microtech', 'diy loop', 'tidepool loop']).isRequired, + deviceKey: PropTypes.oneOf(['animas', 'carelink', 'insulet', 'medtronic', 'microtech', 'diy loop', 'tidepool loop', 'twiist']).isRequired, openedSections: PropTypes.object.isRequired, pumpSettings: PropTypes.shape({ activeSchedule: PropTypes.string.isRequired, diff --git a/src/components/settings/common/Header.js b/src/components/settings/common/Header.js index 28cb9c3a7..0d13f495f 100644 --- a/src/components/settings/common/Header.js +++ b/src/components/settings/common/Header.js @@ -30,7 +30,7 @@ class Header extends PureComponent {
  • - {t('Therapy Settings - Active at Upload on')} {this.props.deviceMeta.uploaded} + {t('Active at Upload on')} {this.props.deviceMeta.uploaded}
diff --git a/src/components/settings/common/PumpSettingsContainer.js b/src/components/settings/common/PumpSettingsContainer.js index 07737cde4..e76945808 100644 --- a/src/components/settings/common/PumpSettingsContainer.js +++ b/src/components/settings/common/PumpSettingsContainer.js @@ -12,7 +12,7 @@ export class PumpSettingsContainer extends PureComponent { bgUnits: PropTypes.oneOf([MGDL_UNITS, MMOLL_UNITS]).isRequired, copySettingsClicked: PropTypes.func.isRequired, manufacturerKey: PropTypes.oneOf( - ['animas', 'carelink', 'insulet', 'medtronic', 'tandem', 'microtech', 'diy loop', 'tidepool loop'] + ['animas', 'carelink', 'insulet', 'medtronic', 'tandem', 'microtech', 'diy loop', 'tidepool loop', 'twiist'] ).isRequired, // see more specific schema in NonTandem and Tandem components! pumpSettings: PropTypes.shape({ @@ -52,7 +52,7 @@ export class PumpSettingsContainer extends PureComponent { timePrefs, toggleSettingsSection, } = this.props; - const supportedNonTandemPumps = ['animas', 'carelink', 'insulet', 'medtronic', 'microtech', 'diy loop', 'tidepool loop']; + const supportedNonTandemPumps = ['animas', 'carelink', 'insulet', 'medtronic', 'microtech', 'diy loop', 'tidepool loop', 'twiist']; const toggleFn = _.partial(toggleSettingsSection, manufacturerKey); if (manufacturerKey === 'tandem') { diff --git a/src/components/settings/common/Table.js b/src/components/settings/common/Table.js index 8f8f5b2b5..34bef9f76 100644 --- a/src/components/settings/common/Table.js +++ b/src/components/settings/common/Table.js @@ -22,7 +22,7 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; import StatTooltip from '../../common/tooltips/StatTooltip'; -import InfoIcon from '../../common/stat/assets/info-outline-24-px.svg'; +import InfoIcon from '../../common/stat/assets/info-outline-24-px.png'; import styles from './Table.css'; class Table extends PureComponent { diff --git a/src/index.js b/src/index.js index 46a48ee2c..237376b82 100644 --- a/src/index.js +++ b/src/index.js @@ -35,7 +35,7 @@ import Stat from './components/common/stat/Stat'; import CBGTooltip from './components/daily/cbgtooltip/CBGTooltip'; import FoodTooltip from './components/daily/foodtooltip/FoodTooltip'; -import { formatBgValue } from './utils/format'; +import { formatBgValue, formatPercentage, bankersRound } from './utils/format'; import { generateBgRangeLabels, isCustomBgRange, reshapeBgClassesToBgBounds } from './utils/bloodglucose'; import { getTotalBasalFromEndpoints, getGroupDurations } from './utils/basal'; import { DEFAULT_BG_BOUNDS } from './utils/constants'; @@ -52,6 +52,8 @@ import { deviceName } from './utils/settings/data'; import { commonStats, + statFormats, + formatDatum, getStatAnnotations, getStatData, getStatDefinition, @@ -62,6 +64,7 @@ import { import { bgLogText } from './utils/bgLog/data'; import { trendsText } from './utils/trends/data'; +import { agpCGMText } from './utils/agp/data'; import TextUtil from './utils/text/TextUtil'; import { generateAGPFigureDefinitions } from './utils/print/plotly'; @@ -126,13 +129,17 @@ const utils = { getTimezoneFromTimePrefs, }, stat: { + bankersRound, commonStats, + formatDatum, + formatPercentage, getStatAnnotations, getStatData, getStatDefinition, getStatTitle, statBgSourceLabels, statFetchMethods, + statFormats, }, settings: { deviceName, @@ -146,6 +153,7 @@ const utils = { trendsText, basicsText, bgLogText, + agpCGMText, }, }; diff --git a/src/modules/print/BasicsPrintView.js b/src/modules/print/BasicsPrintView.js index 62290f5cd..6313d0cb0 100644 --- a/src/modules/print/BasicsPrintView.js +++ b/src/modules/print/BasicsPrintView.js @@ -43,6 +43,7 @@ import { SITE_CHANGE, TIDEPOOL_LOOP, DIY_LOOP, + TWIIST_LOOP, } from '../../utils/constants'; const siteChangeImages = { @@ -52,6 +53,7 @@ const siteChangeImages = { [SITE_CHANGE_TUBING]: 'images/sitechange-tubing.png', [`${TIDEPOOL_LOOP.toLowerCase()}_${SITE_CHANGE_TUBING}`]: 'images/sitechange-loop-tubing.png', [`${DIY_LOOP.toLowerCase()}_${SITE_CHANGE_TUBING}`]: 'images/sitechange-loop-tubing.png', + [`${TWIIST_LOOP.toLowerCase()}_${SITE_CHANGE_RESERVOIR}`]: 'images/sitechange-twiist-cassette.png', }; const t = i18next.t.bind(i18next); @@ -145,7 +147,7 @@ class BasicsPrintView extends PrintView { this.renderCalendarSection({ title: { text: this.sections.boluses.title, - subText: _.get(this.data, 'query.excludeDaysWithoutBolus') ? t('(days with no boluses have been excluded)') : false, + subText: t('(days with no insulin data have been excluded)'), }, data: this.aggregationsByDate.boluses.byDate, type: 'bolus', @@ -226,7 +228,7 @@ class BasicsPrintView extends PrintView { timeInRange, { heading: { - text: 'BG Distribution', + text: 'Time in Range', note: t('Showing {{source}} data', { source: statBgSourceLabels[this.bgSource] }), }, secondaryFormatKey: 'tooltip', @@ -239,7 +241,7 @@ class BasicsPrintView extends PrintView { readingsInRange, { heading: { - text: 'BG Distribution', + text: 'Readings in Range', note: t('{{source}} data from {{count}} readings', { source: statBgSourceLabels[this.bgSource], count: readingsInRange.data?.raw?.counts?.total, @@ -648,10 +650,7 @@ class BasicsPrintView extends PrintView { const xPos = pos.x + padding.left; const yPos = pos.y + padding.top; - const isEmptyBolusDay = color === this.colors.bolus && count === 0; - const isExcludedBolusDay = isEmptyBolusDay && _.get(this.data, 'query.excludeDaysWithoutBolus', false); - - this.setFill((type === 'outOfRange' || isExcludedBolusDay) ? this.colors.lightGrey : 'black', 1); + this.setFill((type === 'outOfRange') ? this.colors.lightGrey : 'black', 1); this.doc .fontSize(this.extraSmallFontSize) diff --git a/src/modules/print/DailyPrintView.js b/src/modules/print/DailyPrintView.js index feee576b3..4f8e74e76 100644 --- a/src/modules/print/DailyPrintView.js +++ b/src/modules/print/DailyPrintView.js @@ -566,7 +566,7 @@ class DailyPrintView extends PrintView { this.doc.fontSize(this.smallFontSize).font(this.boldFont) .text( - t('Average BG'), + t('Avg Glucose'), smallIndent, yPos.update(), { continued: true, width: widthWithoutIndent } diff --git a/src/utils/DataUtil.js b/src/utils/DataUtil.js index 2d92822d0..ccabbbec6 100644 --- a/src/utils/DataUtil.js +++ b/src/utils/DataUtil.js @@ -14,6 +14,7 @@ import { isSettingsOverrideDevice, isDIYLoop, isTidepoolLoop, + isTwiistLoop, } from './device'; import { @@ -38,6 +39,7 @@ import { MGDL_UNITS, DIY_LOOP, TIDEPOOL_LOOP, + TWIIST_LOOP, } from './constants'; import { @@ -94,6 +96,7 @@ export class DataUtil { this.pumpSettingsDatumsByIdMap = this.pumpSettingsDatumsByIdMap || {}; this.wizardDatumsByIdMap = this.wizardDatumsByIdMap || {}; this.wizardToBolusIdMap = this.wizardToBolusIdMap || {}; + this.loopDataSetsByIdMap = this.loopDataSetsByIdMap || {}; this.bolusDosingDecisionDatumsByIdMap = this.bolusDosingDecisionDatumsByIdMap || {}; this.matchedDevices = this.matchedDevices || {}; @@ -188,6 +191,7 @@ export class DataUtil { } if (d.type === 'upload' && d.dataSetType === 'continuous') { + if (isLoop(d)) this.loopDataSetsByIdMap[d.id] = d; if (!d.time) d.time = moment.utc().toISOString(); } @@ -271,7 +275,7 @@ export class DataUtil { const datumToPopulate = _.omit(datumMap[idMap[d.id]], d.type); if (isWizard && d.uploadId !== datumToPopulate.uploadId) { - // Due to an issue stemming from a fix for wizard datums in Ulpoader >= v2.35.0, we have a + // Due to an issue stemming from a fix for wizard datums in Uploader >= v2.35.0, we have a // possibility of duplicates of older wizard datums from previous uploads. The boluses and // corrected wizards should both reference the same uploadId, so we can safely reject // wizards that don't reference the same upload as the bolus it's referencing. @@ -285,7 +289,7 @@ export class DataUtil { }; joinBolusAndDosingDecision = d => { - if (d.type === 'bolus' && isLoop(d)) { + if (d.type === 'bolus' && !!this.loopDataSetsByIdMap[d.uploadId]) { const timeThreshold = MS_IN_MIN; const proximateDosingDecisions = _.filter( @@ -359,6 +363,7 @@ export class DataUtil { override: isOverride(d), underride: isUnderride(d), wizard: !!isWizardOrDosingDecision, + loop: !!this.loopDataSetsByIdMap[d.uploadId], }; } @@ -369,6 +374,12 @@ export class DataUtil { }; } + if (d.type === 'food') { + d.tags = { + loop: !!this.loopDataSetsByIdMap[d.uploadId], + }; + } + if (d.type === 'deviceEvent') { d.tags = { automatedSuspend: ( @@ -534,11 +545,10 @@ export class DataUtil { const isOverrideEvent = d.subType === 'pumpSettingsOverride'; if (_.isFinite(d.duration)) { - // Loop is reporting these durations in seconds instead of the milliseconds historically - // used by Tandem. - // For now, until a fix is present, we'll convert. Once a fix is present, we will only - // convert for Loop versions prior to the fix. - if (isOverrideEvent && isLoop(d)) d.duration = d.duration * 1000; + // DIY and Tidepool Loop are reporting these durations in seconds instead of the milliseconds. + // For now, until a fix is present, we'll convert for Tidepool Loop and DIY Loop. + // Once a fix is present, we will only convert for DIY and Tidepool Loop versions prior to the fix. + if (isOverrideEvent && (isTidepoolLoop(d) || isDIYLoop(d))) d.duration = d.duration * 1000; d.normalEnd = d.normalTime + d.duration; // If the provided duration extends into the future, we truncate the normalEnd to the @@ -835,8 +845,16 @@ export class DataUtil { 'wizard', 'food', ], this.dimension.byType.currentFilter())) { - _.each(this.dimension.byDeviceId.top(Infinity), ({ deviceId }) => { - if (deviceId && !this.matchedDevices[deviceId]) this.matchedDevices[deviceId] = true; + _.each(this.dimension.byDeviceId.top(Infinity), datum => { + const { deviceId, origin } = datum; + + if (deviceId) { + const version = origin?.version || '0.0'; + const deviceName = origin?.name || deviceId; + const deviceVersionId = `${deviceName}_${version}`; + if (!this.matchedDevices[deviceId]) this.matchedDevices[deviceId] = {}; + if (!this.matchedDevices[deviceId][deviceVersionId]) this.matchedDevices[deviceId][deviceVersionId] = true; + } }); } } @@ -891,9 +909,10 @@ export class DataUtil { const deviceModel = _.get(latestPumpUpload, 'deviceModel', ''); const latestPumpSettings = _.cloneDeep(this.latestDatumByType.pumpSettings); - const pumpIsAutomatedBasalDevice = isAutomatedBasalDevice(manufacturer, latestPumpSettings, deviceModel); - const pumpIsAutomatedBolusDevice = isAutomatedBolusDevice(manufacturer, latestPumpSettings); - const pumpIsSettingsOverrideDevice = isSettingsOverrideDevice(manufacturer, latestPumpSettings); + const latestPumpSettingsOrUpload = latestPumpSettings || latestPumpUpload; + const pumpIsAutomatedBasalDevice = isAutomatedBasalDevice(manufacturer, latestPumpSettingsOrUpload, deviceModel); + const pumpIsAutomatedBolusDevice = isAutomatedBolusDevice(manufacturer, latestPumpSettingsOrUpload); + const pumpIsSettingsOverrideDevice = isSettingsOverrideDevice(manufacturer, latestPumpSettingsOrUpload); if (latestPumpSettings && pumpIsAutomatedBasalDevice) { const basalData = this.sort.byTime(this.filter.byType('basal').top(Infinity)); @@ -942,6 +961,8 @@ export class DataUtil { source = TIDEPOOL_LOOP.toLowerCase(); } else if (isDIYLoop(pumpSettings)) { source = DIY_LOOP.toLowerCase(); + } else if (isTwiistLoop(upload)) { + source = TWIIST_LOOP.toLowerCase(); } this.uploadMap[upload.uploadId] = { @@ -1064,6 +1085,8 @@ export class DataUtil { if (deviceManufacturer || deviceModel) { if (deviceManufacturer === 'Dexcom' && isContinuous) { label = t('Dexcom API'); + } else if (deviceManufacturer === 'Abbott' && isContinuous) { + label = t('FreeStyle Libre (from LibreView)'); } else { label = _.reject([deviceManufacturer, deviceModel], _.isEmpty).join(' '); } diff --git a/src/utils/StatUtil.js b/src/utils/StatUtil.js index 0a61a797f..ef408228e 100644 --- a/src/utils/StatUtil.js +++ b/src/utils/StatUtil.js @@ -6,6 +6,7 @@ import { getTotalBasalFromEndpoints, getBasalGroupDurationsFromEndpoints } from import { getTotalBolus } from './bolus'; import { cgmSampleFrequency, classifyBgValue } from './bloodglucose'; import { BGM_DATA_KEY, MGDL_UNITS, MGDL_PER_MMOLL, MS_IN_DAY, MS_IN_MIN } from './constants'; +import { formatLocalizedFromUTC } from './datetime'; /* eslint-disable lodash/prefer-lodash-method, no-underscore-dangle, no-param-reassign */ @@ -24,8 +25,8 @@ export class StatUtil { this.bgUnits = _.get(dataUtil, 'bgPrefs.bgUnits'); this.bgSource = _.get(dataUtil, 'bgSources.current', BGM_DATA_KEY); this.activeDays = dataUtil.activeEndpoints.activeDays; - this.bolusDays = dataUtil.activeEndpoints.bolusDays || this.activeDays; this.endpoints = dataUtil.activeEndpoints.range; + this.timePrefs = _.get(dataUtil, 'timePrefs'); this.log('activeDays', this.activeDays); this.log('bgSource', this.bgSource); @@ -82,6 +83,14 @@ export class StatUtil { const rawBasalData = this.dataUtil.sort.byTime(this.dataUtil.filter.byType('basal').top(Infinity)); const basalData = this.dataUtil.addBasalOverlappingStart(_.cloneDeep(rawBasalData)); + // Create a list of all dates for which we have at least one datum + const uniqueDatumDates = new Set([ + ...bolusData.map(datum => formatLocalizedFromUTC(datum.time, this.timePrefs, 'YYYY-MM-DD')), + ...rawBasalData.map(datum => formatLocalizedFromUTC(datum.time, this.timePrefs, 'YYYY-MM-DD')), + ]); + + const activeDaysWithInsulinData = uniqueDatumDates.size; + const basalBolusData = { basal: basalData.length ? parseFloat(getTotalBasalFromEndpoints(basalData, this.endpoints)) @@ -89,9 +98,9 @@ export class StatUtil { bolus: bolusData.length ? getTotalBolus(bolusData) : NaN, }; - if (this.bolusDays > 1) { - basalBolusData.basal = basalBolusData.basal / this.bolusDays; - basalBolusData.bolus = basalBolusData.bolus / this.bolusDays; + if (activeDaysWithInsulinData > 1) { + basalBolusData.basal = basalBolusData.basal / activeDaysWithInsulinData; + basalBolusData.bolus = basalBolusData.bolus / activeDaysWithInsulinData; } return basalBolusData; @@ -101,6 +110,14 @@ export class StatUtil { const wizardData = this.dataUtil.filter.byType('wizard').top(Infinity); const foodData = this.dataUtil.filter.byType('food').top(Infinity); + // Create a list of all dates for which we have at least one datum + const uniqueDatumDates = new Set([ + ...wizardData.map(datum => formatLocalizedFromUTC(datum.time, this.timePrefs, 'YYYY-MM-DD')), + ...foodData.map(datum => formatLocalizedFromUTC(datum.time, this.timePrefs, 'YYYY-MM-DD')), + ]); + + const activeDaysWithCarbData = uniqueDatumDates.size; + const wizardCarbs = _.reduce( wizardData, (result, datum) => { @@ -132,10 +149,10 @@ export class StatUtil { exchanges: wizardCarbs.exchanges, }; - if (this.activeDays > 1) { + if (activeDaysWithCarbData > 1) { carbs = { - grams: carbs.grams / this.activeDays, - exchanges: carbs.exchanges / this.activeDays, + grams: carbs.grams / activeDaysWithCarbData, + exchanges: carbs.exchanges / activeDaysWithCarbData, }; } diff --git a/src/utils/agp/data.js b/src/utils/agp/data.js new file mode 100644 index 000000000..dc6effff4 --- /dev/null +++ b/src/utils/agp/data.js @@ -0,0 +1,118 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2016, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +import _ from 'lodash'; +import i18next from 'i18next'; +import moment from 'moment'; + +import TextUtil from '../text/TextUtil'; +import { formatPercentage } from '../format'; +import { formatDatum } from '../../utils/stat'; + +import { + getOffset, + getTimezoneFromTimePrefs, + formatCurrentDate, + formatDateRange, + formatDuration, +} from '../datetime'; + +import { MS_IN_MIN } from '../constants'; + +const t = i18next.t.bind(i18next); + +/** + * agpCGMText + * @param {Object} patient - the patient object that contains the profile + * @param {Object} data - agpCGM object outputted from generatePDF + * + * @return {String} agpCGM data as a formatted string + */ +export function agpCGMText(patient, data) { + if (!data || !patient) return ''; + + 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 { fullName, birthDate } = patient; + + const { + timePrefs, + bgPrefs, + data: { + current: { + stats: { + bgExtents: { newestDatum, oldestDatum, bgDaysWorn }, + averageGlucose: { averageGlucose }, + timeInRange: { counts, durations }, + }, + }, + }, + } = data; + + const { bgUnits, bgBounds } = bgPrefs || {}; + const { targetUpperBound, targetLowerBound, veryLowThreshold } = bgBounds || {}; + + const timezone = getTimezoneFromTimePrefs(timePrefs); + + const currentDate = formatCurrentDate(); + + 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); + + const targetRange = `${targetLowerBound}-${targetUpperBound}`; + const lowRange = `${veryLowThreshold}-${targetLowerBound}`; + const veryLowRange = `<${veryLowThreshold}`; + + const percentInTarget = formatPercentage(counts.target / counts.total, 0, true); + const percentInLow = formatPercentage(counts.low / counts.total, 0, true); + const percentInVeryLow = formatPercentage(counts.veryLow / counts.total, 0, true); + + const durationInTarget = formatDuration(durations.target, { condensed: true }); + const durationInLow = formatDuration(durations.low, { condensed: true }); + const durationInVeryLow = formatDuration(durations.veryLow, { condensed: true }); + + const avgGlucose = averageGlucose ? formatDatum({ value: averageGlucose }, 'bgValue', { bgPrefs, useAGPFormat: true })?.value : null; + + 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: {{reportDaysText}}', { reportDaysText })); + clipboardText += textUtil.buildTextLine(''); + clipboardText += textUtil.buildTextLine(t('Avg. Daily Time In Range ({{- bgUnits}})', { bgUnits })); + clipboardText += textUtil.buildTextLine(t('{{targetRange}} {{percentInTarget}} ({{ durationInTarget }})', { targetRange, percentInTarget, durationInTarget })); + clipboardText += textUtil.buildTextLine(t('{{lowRange}} {{percentInLow}} ({{ durationInLow }})', { lowRange, percentInLow, durationInLow })); + clipboardText += textUtil.buildTextLine(t('{{- veryLowRange}} {{percentInVeryLow}} ({{ durationInVeryLow }})', { veryLowRange, percentInVeryLow, durationInVeryLow })); + clipboardText += textUtil.buildTextLine(''); + clipboardText += textUtil.buildTextLine(t('Avg. Glucose (CGM): {{avgGlucose}} {{- bgUnits}}', { avgGlucose, bgUnits })); + + return clipboardText; +} diff --git a/src/utils/annotations.js b/src/utils/annotations.js index bbfc4cd9c..65366c0d0 100644 --- a/src/utils/annotations.js +++ b/src/utils/annotations.js @@ -112,6 +112,7 @@ export function getMedtronic600AnnotationMessages(datum) { */ export function getOutOfRangeAnnotationMessage(datum) { const annotations = getAnnotations(datum); + const bgTypeLabel = datum?.type === 'cbg' ? t('glucose') : t('BG'); const messages = []; _.each(annotations, annotation => { if (_.get(annotation, 'code', '') === 'bg/out-of-range') { @@ -119,7 +120,7 @@ export function getOutOfRangeAnnotationMessage(datum) { messages.push( _.assign({}, annotation, { message: { - value: t('* This BG value was {{value}}er than your device could record. Your actual BG value is {{value}}er than it appears here.', { value }), + value: t('* This {{bgTypeLabel}} value was {{value}}er than your device could record. Your actual {{bgTypeLabel}} value is {{value}}er than it appears here.', { value, bgTypeLabel }), }, }) ); diff --git a/src/utils/basal.js b/src/utils/basal.js index ee3988f6b..2140ca65d 100644 --- a/src/utils/basal.js +++ b/src/utils/basal.js @@ -175,6 +175,32 @@ export function getSegmentDose(duration, rate) { return parseFloat(precisionRound(hours * rate, 3)); } +/** + * Get the duration of a basal datum within a given time range + * @param {Object} datum A single basal datum + * @param {[]String} enpoints ISO date strings for the start, end of the range, in that order + * @returns {Number} Duration of the basal datum falling within the range in milliseconds + */ +export function getBasalDurationWithinRange(datum, endpoints) { + const rangeStart = new Date(endpoints[0]).valueOf(); + const rangeEnd = new Date(endpoints[1]).valueOf(); + const datumStart = new Date(datum.normalTime).valueOf(); + const datumEnd = new Date(datum.normalEnd).valueOf(); + + const datumStartIsWithinRange = rangeStart <= datumStart && datumStart < rangeEnd; + const datumEndIsWithinRange = rangeStart < datumEnd && datumEnd <= rangeEnd; + const datumEncompassesRange = rangeStart >= datumStart && datumEnd >= rangeEnd; + + const trimmedStart = _.max([rangeStart, datumStart]); + const trimmedEnd = _.min([rangeEnd, datumEnd]); + + if (datumStartIsWithinRange || datumEndIsWithinRange || datumEncompassesRange) { + return trimmedEnd - trimmedStart; + } + + return 0; +} + /** * Get total basal delivered for a given time range * @param {Array} data Array of Tidepool basal data @@ -182,30 +208,10 @@ export function getSegmentDose(duration, rate) { * @return {Number} Formatted total insulin dose */ export function getTotalBasalFromEndpoints(data, endpoints) { - const start = new Date(endpoints[0]); - const end = new Date(endpoints[1]); let dose = 0; _.each(data, datum => { - const datumStart = new Date(datum.normalTime); - const datumEnd = new Date(datum.normalEnd); - - const datumStartIsWithinRange = start.valueOf() <= datumStart.valueOf() && datumStart.valueOf() < end.valueOf(); - const datumEndIsWithinRange = start.valueOf() < datumEnd.valueOf() && datumEnd.valueOf() <= end.valueOf(); - - if (datumStartIsWithinRange || datumEndIsWithinRange) { - let duration = 0; - - if (datumStartIsWithinRange && datumEndIsWithinRange) { - duration = datum.duration; - } else if (datumEndIsWithinRange) { - duration = _.min([datumEnd - start, datum.duration]); - } else if (datumStartIsWithinRange) { - duration = _.min([end - datumStart, datum.duration]); - } - - dose += getSegmentDose(duration, datum.rate); - } + dose += getSegmentDose(getBasalDurationWithinRange(datum, endpoints), datum.rate); }); return formatInsulin(dose); @@ -218,24 +224,13 @@ export function getTotalBasalFromEndpoints(data, endpoints) { * @return {Number} Formatted total insulin dose */ export function getBasalGroupDurationsFromEndpoints(data, endpoints) { - const start = new Date(endpoints[0]); - const end = new Date(endpoints[1]); - const durations = { automated: 0, manual: 0, }; - _.each(data, (datum, index) => { - let duration = datum.duration; - if (index === 0) { - // handle first segment, which may have started before the start endpoint - duration = _.min([new Date(datum.normalEnd) - start, datum.duration]); - } else if (index === data.length - 1) { - // handle last segment, which may go past the end endpoint - duration = _.min([end - new Date(datum.normalTime), datum.duration]); - } - durations[getBasalPathGroupType(datum)] += duration; + _.each(data, datum => { + durations[getBasalPathGroupType(datum)] += getBasalDurationWithinRange(datum, endpoints); }); return durations; diff --git a/src/utils/basics/data.js b/src/utils/basics/data.js index d1a205b2f..60f72977f 100644 --- a/src/utils/basics/data.js +++ b/src/utils/basics/data.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import moment from 'moment'; import i18next from 'i18next'; -import { getPumpVocabulary, getUppercasedManufacturer, isLoop } from '../device'; +import { getPumpVocabulary, getUppercasedManufacturer, isDIYLoop, isLoop, isTidepoolLoop, isTwiistLoop } from '../device'; import { generateBgRangeLabels, reshapeBgClassesToBgBounds, @@ -41,6 +41,7 @@ import { MICROTECH, pumpVocabulary, TIDEPOOL_LOOP, + TWIIST_LOOP, } from '../constants'; import TextUtil from '../text/TextUtil'; @@ -86,6 +87,9 @@ export function defineBasicsAggregations(bgPrefs, manufacturer, pumpUpload = {}) let summaryTitle; let perRow = 3; + // TODO: Remove this once we have twiist bolus and wizards linked + const hideEmptyWizardDimensions = isTwiistLoop(pumpUpload.settings); + switch (section) { case 'basals': title = 'Basals'; @@ -118,15 +122,15 @@ export function defineBasicsAggregations(bgPrefs, manufacturer, pumpUpload = {}) summaryTitle = t('Avg boluses / day'); dimensions = [ { path: 'summary', key: 'total', label: t('Avg per day'), average: true, primary: true }, - { path: 'summary.subtotals', key: 'wizard', label: t('Calculator'), percentage: true, selectorIndex: 0 }, - { path: 'summary.subtotals', key: 'correction', label: t('Correction'), percentage: true, selectorIndex: 1 }, + { path: 'summary.subtotals', key: 'wizard', label: t('Calculator'), percentage: true, selectorIndex: 0, hideEmpty: hideEmptyWizardDimensions }, + { path: 'summary.subtotals', key: 'correction', label: t('Correction'), percentage: true, selectorIndex: 1, hideEmpty: hideEmptyWizardDimensions }, { path: 'summary.subtotals', key: 'extended', label: t('Extended'), percentage: true, selectorIndex: 4 }, { path: 'summary.subtotals', key: 'interrupted', label: t('Interrupted'), percentage: true, selectorIndex: 5 }, - { path: 'summary.subtotals', key: 'override', label: t('Override'), percentage: true, selectorIndex: 2 }, - { path: 'summary.subtotals', key: 'underride', label: t('Underride'), percentage: true, selectorIndex: 6 }, + { path: 'summary.subtotals', key: 'override', label: t('Override'), percentage: true, selectorIndex: 2, hideEmpty: hideEmptyWizardDimensions }, + { path: 'summary.subtotals', key: 'underride', label: t('Underride'), percentage: true, selectorIndex: 6, hideEmpty: hideEmptyWizardDimensions }, ]; - if (isLoop(pumpUpload.settings)) { + if (isTidepoolLoop(pumpUpload.settings) || isDIYLoop(pumpUpload.settings)) { dimensions[1].label = t('Meal'); dimensions.splice(3, 1); dimensions[2].selectorIndex = 6; @@ -156,7 +160,7 @@ export function defineBasicsAggregations(bgPrefs, manufacturer, pumpUpload = {}) break; case 'siteChanges': - title = t('Infusion site changes'); + title = t('Site Changes'); break; default: @@ -208,10 +212,17 @@ export function getSiteChangeSource(patient = {}, manufacturer) { const allowedSources = [SITE_CHANGE_CANNULA, SITE_CHANGE_TUBING]; if (!_.includes(allowedSources, siteChangeSource)) { - siteChangeSource = SITE_CHANGE_TYPE_UNDECLARED; + siteChangeSource = SITE_CHANGE_CANNULA; } } else if (_.includes(_.map([INSULET, MICROTECH], _.lowerCase), manufacturer)) { siteChangeSource = SITE_CHANGE_RESERVOIR; + } else if (_.includes(_.map([TWIIST_LOOP], _.lowerCase), manufacturer)) { + siteChangeSource = _.get(settings, 'siteChangeSource'); + const allowedSources = [SITE_CHANGE_CANNULA, SITE_CHANGE_RESERVOIR]; + + if (!_.includes(allowedSources, siteChangeSource)) { + siteChangeSource = SITE_CHANGE_RESERVOIR; + } } else if (_.includes(_.map([DIY_LOOP, TIDEPOOL_LOOP], _.lowerCase), manufacturer)) { siteChangeSource = SITE_CHANGE_TUBING; } @@ -265,9 +276,7 @@ export function processBasicsAggregations(aggregations, data, patient, manufactu break; case 'siteChanges': - emptyText = hasDataInRange(aggregationData[aggregationKey]) - ? t("Please choose a preferred site change source from the 'Basics' web view to view this data.") - : t("This section requires data from an insulin pump, so there's nothing to display."); + emptyText = t("This section requires data from an insulin pump, so there's nothing to display."); break; case 'fingersticks': @@ -300,7 +309,7 @@ export function processBasicsAggregations(aggregations, data, patient, manufactu } else if (type === 'siteChanges') { aggregations[key].source = getSiteChangeSource(patient, manufacturer); aggregations[key].manufacturer = manufacturer; - disabled = aggregations[key].source === SITE_CHANGE_TYPE_UNDECLARED; + disabled = !hasDataInRange(aggregationData[type]); if (!disabled) { aggregations[key].subTitle = getSiteChangeSourceLabel( aggregations[key].source, @@ -377,7 +386,6 @@ export function basicsText(patient, data, stats, aggregations) { }, }, metaData, - query, timePrefs, } = data; @@ -386,9 +394,7 @@ export function basicsText(patient, data, stats, aggregations) { let basicsString = textUtil.buildDocumentHeader('Basics'); basicsString += textUtil.buildDocumentDates(); - if (_.get(query, 'excludeDaysWithoutBolus')) { - basicsString += textUtil.buildTextLine(t('Days with no boluses have been excluded from bolus calculations')); - } + basicsString += textUtil.buildTextLine(t('Days with no insulin data have been excluded from calculations')); basicsString += utils.statsText(stats, textUtil, bgPrefs); diff --git a/src/utils/bloodglucose.js b/src/utils/bloodglucose.js index b20fe462d..ad551d8f4 100644 --- a/src/utils/bloodglucose.js +++ b/src/utils/bloodglucose.js @@ -230,6 +230,11 @@ export function weightedCGMCount(data) { */ export function cgmSampleFrequency(datum) { const deviceId = _.get(datum, 'deviceId', ''); + + if (datum?.sampleInterval) { + return datum.sampleInterval; + } + if (deviceId.indexOf('AbbottFreeStyleLibre3') === 0) { return 5 * MS_IN_MIN; } @@ -238,10 +243,6 @@ export function cgmSampleFrequency(datum) { return 15 * MS_IN_MIN; } - if (deviceId.indexOf('tandemCIQ') === 0 && _.get(datum, 'payload.fsl2')) { - return MS_IN_MIN; - } - return 5 * MS_IN_MIN; } diff --git a/src/utils/constants.js b/src/utils/constants.js index 8daf3eabc..4dc45d81a 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -100,6 +100,7 @@ export const TANDEM = 'Tandem'; export const ANIMAS = 'Animas'; export const TIDEPOOL_LOOP = 'Tidepool Loop'; export const DIY_LOOP = 'DIY Loop'; +export const TWIIST_LOOP = 'twiist'; export const MEDTRONIC = 'Medtronic'; export const MICROTECH = 'Microtech'; @@ -107,11 +108,11 @@ export const pumpVocabulary = { [ANIMAS]: { [SITE_CHANGE_RESERVOIR]: t('Go Rewind'), [SITE_CHANGE_TUBING]: t('Go Prime'), - [SITE_CHANGE_CANNULA]: t('Fill Cannula'), + [SITE_CHANGE_CANNULA]: t('Cannula Fill'), }, [INSULET]: { - [SITE_CHANGE_RESERVOIR]: t('Change Pod'), - [SITE_CHANGE_TUBING]: t('Activate Pod'), + [SITE_CHANGE_RESERVOIR]: t('Pod Change'), + [SITE_CHANGE_TUBING]: t('Pod Activate'), [SITE_CHANGE_CANNULA]: t('Prime'), [MAX_BOLUS]: t('Maximum Bolus'), [MAX_BASAL]: t('Max Basal Rate'), @@ -120,7 +121,7 @@ export const pumpVocabulary = { [MEDTRONIC]: { [SITE_CHANGE_RESERVOIR]: t('Rewind'), [SITE_CHANGE_TUBING]: t('Prime'), - [SITE_CHANGE_CANNULA]: t('Prime Cannula'), + [SITE_CHANGE_CANNULA]: t('Cannula Prime'), [AUTOMATED_DELIVERY]: t('Auto Mode'), [SCHEDULED_DELIVERY]: t('Manual'), [MAX_BOLUS]: t('Max Bolus'), @@ -129,13 +130,13 @@ export const pumpVocabulary = { }, [MICROTECH]: { [SITE_CHANGE_RESERVOIR]: t('Rewind'), - [SITE_CHANGE_TUBING]: t('Prime Reservoir'), - [SITE_CHANGE_CANNULA]: t('Prime Cannula'), + [SITE_CHANGE_TUBING]: t('Reservoir Prime'), + [SITE_CHANGE_CANNULA]: t('Cannula Prime'), }, [TANDEM]: { - [SITE_CHANGE_RESERVOIR]: t('Change Cartridge'), - [SITE_CHANGE_TUBING]: t('Fill Tubing'), - [SITE_CHANGE_CANNULA]: t('Fill Cannula'), + [SITE_CHANGE_RESERVOIR]: t('Cartridge Change'), + [SITE_CHANGE_TUBING]: t('Tubing Fill'), + [SITE_CHANGE_CANNULA]: t('Cannula Fill'), [AUTOMATED_DELIVERY]: t('Automation'), [SCHEDULED_DELIVERY]: t('Manual'), [SETTINGS_OVERRIDE]: t('Activity'), @@ -153,6 +154,16 @@ export const pumpVocabulary = { [MAX_BOLUS]: t('Maximum Bolus'), [MAX_BASAL]: t('Maximum Basal Rate'), }, + [TWIIST_LOOP]: { + [SITE_CHANGE_RESERVOIR]: t('Cassette Change'), + [AUTOMATED_DELIVERY]: t('Automation'), + [AUTOMATED_MODE_EXITED]: t('Off'), + [SCHEDULED_DELIVERY]: t('Manual'), + [SETTINGS_OVERRIDE]: t('Preset'), + [PHYSICAL_ACTIVITY]: { label: t('Workout'), marker: t('W') }, + [MAX_BOLUS]: t('Maximum Bolus'), + [MAX_BASAL]: t('Maximum Basal Rate'), + }, [DIY_LOOP]: { [AUTOMATED_DELIVERY]: t('Automation'), [AUTOMATED_MODE_EXITED]: t('Off'), @@ -163,9 +174,9 @@ export const pumpVocabulary = { [MAX_BASAL]: t('Maximum Basal Rate'), }, default: { - [SITE_CHANGE_RESERVOIR]: t('Change Cartridge'), - [SITE_CHANGE_TUBING]: t('Fill Tubing'), - [SITE_CHANGE_CANNULA]: t('Fill Cannula'), + [SITE_CHANGE_RESERVOIR]: t('Cartridge Change'), + [SITE_CHANGE_TUBING]: t('Tubing Fill'), + [SITE_CHANGE_CANNULA]: t('Cannula Fill'), [AUTOMATED_DELIVERY]: t('Automated'), [AUTOMATED_SUSPEND]: t('Automated Suspend'), [AUTOMATED_MODE_EXITED]: t('Exited'), @@ -189,6 +200,10 @@ export const settingsOverrides = { PHYSICAL_ACTIVITY, PREPRANDIAL, ], + [TWIIST_LOOP]: [ + PHYSICAL_ACTIVITY, + PREPRANDIAL, + ], [DIY_LOOP]: [ PREPRANDIAL, ], diff --git a/src/utils/datetime.js b/src/utils/datetime.js index 11178d082..450fc9cd8 100644 --- a/src/utils/datetime.js +++ b/src/utils/datetime.js @@ -312,19 +312,30 @@ export function getLocalizedCeiling(utc, timePrefs) { export const formatTimeAgo = (utc, timePrefs, format = 'YYYY-MM-DD') => { const timezone = getTimezoneFromTimePrefs(timePrefs); const endOfToday = moment.utc(getLocalizedCeiling(new Date().toISOString(), timePrefs)).tz(timezone); - const endOfLastUploadDay = moment.utc(getLocalizedCeiling(utc, timePrefs)).tz(timezone); - const daysAgo = endOfToday.diff(endOfLastUploadDay, 'days', true); + const endOfProvidedDay = moment.utc(getLocalizedCeiling(utc, timePrefs)).tz(timezone); + const daysAgo = endOfToday.diff(endOfProvidedDay, 'days', true); + const minutesAgo = moment.utc().tz(timezone).diff(utc, 'minutes'); + const hoursAgo = moment.utc().tz(timezone).diff(utc, 'hours'); const lastUploadDateMoment = moment.utc(utc).tz(timezone); - let text = lastUploadDateMoment.format(format); + let daysText = lastUploadDateMoment.format(format); if (daysAgo < 2) { - text = (daysAgo >= 1) ? t('Yesterday') : t('Today'); + daysText = (daysAgo >= 1) ? t('yesterday') : t('today'); } else if (daysAgo <= 30) { - text = t('{{days}} days ago', { days: Math.ceil(daysAgo) }); + daysText = t('{{days}} days ago', { days: Math.ceil(daysAgo) }); } + const hoursText = t('{{hoursAgo}} {{unit}} ago', { hoursAgo, unit: hoursAgo === 1 ? 'hour' : 'hours' }); + + let minutesText = t('{{minutesAgo}} {{unit}} ago', { minutesAgo, unit: minutesAgo === 1 ? 'minute' : 'minutes' }); + if (minutesAgo < 1) minutesText = t('a few seconds ago'); + return { daysAgo, - text, + daysText, + hoursAgo, + hoursText, + minutesAgo, + minutesText, }; }; diff --git a/src/utils/device.js b/src/utils/device.js index a81f61060..2371000d7 100644 --- a/src/utils/device.js +++ b/src/utils/device.js @@ -41,46 +41,57 @@ export function isTidepoolLoop(datum = {}) { return (/^org\.tidepool\.[a-zA-Z0-9]*\.?Loop/).test(_.get(datum, 'origin.name', datum?.client?.name || '')); } +/** + * Check to see datum is from Twiist Loop +*/ +export function isTwiistLoop(datum = {}) { + if (datum.type === 'upload') { + const majorVersion = parseInt(_.get(datum, 'client.version', '0').split('.')[0], 10); + return (/^com.sequelmedtech.tidepool-service/).test(_.get(datum, 'client.name', '')) && majorVersion >= 2; + } + return (/^com.dekaresearch.twiist/).test(_.get(datum, 'origin.name', datum?.client?.name || '')); +} + /** * Check to see if datum is from a known Loop device */ export function isLoop(datum = {}) { - return isDIYLoop(datum) || isTidepoolLoop(datum); + return datum.tags?.loop || isDIYLoop(datum) || isTidepoolLoop(datum) || isTwiistLoop(datum); } /** - * Check if the provided upload datum was for an automated basal device + * Check if the provided datum was for an automated basal device * @param {String} manufacturer Manufacturer name - * @param {Object} pumpSettings Tidepool pumpSettings datum + * @param {Object} pumpSettingsOrUpload Tidepool pumpSettings or upload datum * @param {String} deviceModel Device model number * @returns {Boolean} */ -export function isAutomatedBasalDevice(manufacturer, pumpSettings = {}, deviceModel) { +export function isAutomatedBasalDevice(manufacturer, pumpSettingsOrUpload = {}, deviceModel) { return _.includes(_.get(AUTOMATED_BASAL_DEVICE_MODELS, deviceName(manufacturer), []), deviceModel) - || (manufacturer === 'tandem' && _.get(pumpSettings, 'deviceId', '').indexOf('tandemCIQ') === 0) - || isLoop(pumpSettings); + || (manufacturer === 'tandem' && _.get(pumpSettingsOrUpload, 'deviceId', '').indexOf('tandemCIQ') === 0) + || isLoop(pumpSettingsOrUpload); } /** - * Check if the provided upload datum was for an automated bolus device + * Check if the provided datum was for an automated bolus device * @param {String} manufacturer Manufacturer name - * @param {Object} pumpSettings Tidepool pumpSettings datum + * @param {Object} pumpSettingsOrUpload Tidepool pumpSettings or upload datum * @returns {Boolean} */ -export function isAutomatedBolusDevice(manufacturer, pumpSettings = {}) { - return (manufacturer === 'tandem' && _.get(pumpSettings, 'deviceId', '').indexOf('tandemCIQ') === 0) - || isDIYLoop(pumpSettings); +export function isAutomatedBolusDevice(manufacturer, pumpSettingsOrUpload = {}) { + return (manufacturer === 'tandem' && _.get(pumpSettingsOrUpload, 'deviceId', '').indexOf('tandemCIQ') === 0) + || isDIYLoop(pumpSettingsOrUpload); } /** - * Check if the provided upload datum was for a settings-overrideable device + * Check if the provided datum was for a settings-overrideable device * @param {String} manufacturer Manufacturer name - * @param {Object} pumpSettings Tidepool pumpSettings datum + * @param {Object} pumpSettingsOrUpload Tidepool pumpSettings or upload datum * @returns {Boolean} */ -export function isSettingsOverrideDevice(manufacturer, pumpSettings = {}) { - return (manufacturer === 'tandem' && _.get(pumpSettings, 'deviceId', '').indexOf('tandemCIQ') === 0) - || isLoop(pumpSettings); +export function isSettingsOverrideDevice(manufacturer, pumpSettingsOrUpload = {}) { + return (manufacturer === 'tandem' && _.get(pumpSettingsOrUpload, 'deviceId', '').indexOf('tandemCIQ') === 0) + || isLoop(pumpSettingsOrUpload); } /** @@ -88,7 +99,16 @@ export function isSettingsOverrideDevice(manufacturer, pumpSettings = {}) { * @param {String} manufacturer Manufacturer name */ export function getUppercasedManufacturer(manufacturer = '') { - return _.map(manufacturer.split(' '), part => (part === 'diy' ? _.upperCase(part) : _.upperFirst(part))).join(' '); + return _.map(manufacturer.split(' '), part => { + switch (part) { + case 'diy': + return _.upperCase(part); + case 'twiist': + return part; + default: + return _.upperFirst(part); + } + }).join(' '); } /** diff --git a/src/utils/settings/data.js b/src/utils/settings/data.js index c041ef4f4..6ceb4e428 100644 --- a/src/utils/settings/data.js +++ b/src/utils/settings/data.js @@ -58,6 +58,7 @@ export function deviceName(manufacturer) { microtech: 'Equil', 'diy loop': 'DIY Loop', 'tidepool loop': 'Tidepool Loop', + twiist: 'twiist', }; return DEVICE_DISPLAY_NAME_BY_MANUFACTURER[manufacturer] || manufacturer; } @@ -158,7 +159,7 @@ export function getTotalBasalRates(scheduleData) { * getScheduleLabel * @param {String} scheduleName basal schedule name * @param {String} activeName name of active basal schedule at time of upload - * @param {String} deviceKey one of: animas, carelink, insulet, medtronic, tandem, microtech, tidepool loop, diy loop + * @param {String} deviceKey one of: animas, carelink, insulet, medtronic, tandem, microtech, tidepool loop, diy loop, twiist * @param {Boolean} noUnits whether units should be included in label object * * @return {Object} object representing basal schedule label @@ -389,7 +390,7 @@ export function startTimeAndValue(valueKey) { * insulinSettings * * @param {Object} settings object with pump settings data - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, tandem, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, tandem, microtech, tidepool loop, diy loop, twiist * @param {String} [scheduleName] name of schedule for tandem settings */ export function insulinSettings(settings, manufacturer, scheduleName) { @@ -400,7 +401,7 @@ export function insulinSettings(settings, manufacturer, scheduleName) { let insulinDurationUnits = _.get(settings, scheduleName ? `bolus[${scheduleName}].calculator.insulin.units` : 'bolus.calculator.insulin.units'); let insulinDuration = _.get(settings, scheduleName ? `bolus[${scheduleName}].calculator.insulin.duration` : 'bolus.calculator.insulin.duration'); - if (_.includes(['diy loop', 'tidepool loop'], manufacturer)) { + if (_.includes(['diy loop', 'tidepool loop', 'twiist'], manufacturer)) { insulinDuration = _.get(settings, 'insulinModel.actionDuration'); insulinDurationUnits = 'milliseconds'; } @@ -482,7 +483,7 @@ export function insulinSettings(settings, manufacturer, scheduleName) { * presetSettings * * @param {Object} settings object with pump settings data - * @param {String} manufacturer one of: tidepool loop, diy loop + * @param {String} manufacturer one of: tidepool loop, diy loop, twiist */ export function presetSettings(settings, manufacturer) { const deviceLabels = getPumpVocabulary(manufacturer); diff --git a/src/utils/settings/nonTandemData.js b/src/utils/settings/nonTandemData.js index c0a724311..8e24492d3 100644 --- a/src/utils/settings/nonTandemData.js +++ b/src/utils/settings/nonTandemData.js @@ -45,7 +45,7 @@ export function deviceMeta(settings, timePrefs) { /** * bolusTitle - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop, twiist * * @return {String} bolus title for given manufacturer */ @@ -57,6 +57,7 @@ export function bolusTitle(manufacturer) { microtech: t('Bolus Calculator'), 'tidepool loop': t('Bolus Calculator'), 'diy loop': t('Bolus Calculator'), + twiist: t('Bolus Calculator'), }; return BOLUS_SETTINGS_LABEL_BY_MANUFACTURER[manufacturer]; } @@ -89,7 +90,7 @@ function basalColumns() { * basal * * @param {Object} settings object with pump settings data - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop, twiist * @return {Object} object with basal title, columns and rows */ export function basal(schedule, settings, manufacturer) { @@ -123,6 +124,7 @@ function sensitivityTitle(manufacturer) { microtech: t('Insulin Sensitivity'), 'diy loop': t('Insulin Sensitivities'), 'tidepool loop': t('Insulin Sensitivities'), + twiist: t('Insulin Sensitivities'), }; return ISF_BY_MANUFACTURER[manufacturer]; } @@ -174,6 +176,7 @@ function ratioTitle(manufacturer) { microtech: t('Carbohydrate Ratio'), 'diy loop': t('Carb Ratios'), 'tidepool loop': t('Carb Ratios'), + twiist: t('Carb Ratios'), }; return CARB_RATIO_BY_MANUFACTURER[manufacturer]; } @@ -198,7 +201,7 @@ function ratioRows(settings) { * ratio * * @param {Object} settings object with pump settings data - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop, twiist * @return {Object} object with ratio title, columns and rows */ export function ratio(settings, manufacturer) { @@ -221,6 +224,7 @@ function targetTitle(manufacturer) { microtech: t('Target BG'), 'diy loop': t('Correction Range'), 'tidepool loop': t('Correction Range'), + twiist: t('Correction Range'), }; return BG_TARGET_BY_MANUFACTURER[manufacturer]; } @@ -261,6 +265,11 @@ function targetColumns(manufacturer) { { key: 'columnTwo', label: t('Low') }, { key: 'columnThree', label: t('High') }, ], + twiist: [ + { key: 'start', label: t('Start time') }, + { key: 'columnTwo', label: t('Low') }, + { key: 'columnThree', label: t('High') }, + ], }; return BG_TARGET_COLS_BY_MANUFACTURER[manufacturer]; } @@ -277,8 +286,9 @@ function targetRows(settings, units, manufacturer) { microtech: { columnTwo: 'low', columnThree: 'high' }, 'diy loop': { columnTwo: 'low', columnThree: 'high' }, 'tidepool loop': { columnTwo: 'low', columnThree: 'high' }, + twiist: { columnTwo: 'low', columnThree: 'high' }, }; - const targetData = _.includes(['diy loop', 'tidepool loop'], manufacturer) + const targetData = _.includes(['diy loop', 'tidepool loop', 'twiist'], manufacturer) ? settings.bgTargets[settings.activeSchedule] : settings.bgTarget; @@ -293,7 +303,7 @@ function targetRows(settings, units, manufacturer) { * target * * @param {Object} settings object with pump settings data - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop, twiist * @param {String} units MGDL_UNITS or MMOLL_UNITS * @return {Object} object with target title, columns and rows */ diff --git a/src/utils/settings/textData.js b/src/utils/settings/textData.js index f86debddc..29463865b 100644 --- a/src/utils/settings/textData.js +++ b/src/utils/settings/textData.js @@ -29,7 +29,7 @@ const t = i18next.t.bind(i18next); * nonTandemText * @param {Object} patient the patient object that contains the profile * @param {String} units MGDL_UNITS or MMOLL_UNITS - * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop + * @param {String} manufacturer one of: animas, carelink, insulet, medtronic, microtech, tidepool loop, diy loop, twiist * * @return {String} non tandem settings as a string table */ diff --git a/src/utils/stat.js b/src/utils/stat.js index e239d2f24..b712885cd 100644 --- a/src/utils/stat.js +++ b/src/utils/stat.js @@ -318,7 +318,7 @@ export const getStatAnnotations = (data, type, opts = {}) => { case commonStats.averageDailyDose: if (days > 1) { - annotations.push(t('**Avg. Daily Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view.')); + annotations.push(t('**Avg. Daily Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view for which we have insulin data.')); } else { annotations.push(t('**Daily Insulin:** All basal and bolus insulin delivery (in Units) added together.')); } @@ -326,7 +326,7 @@ export const getStatAnnotations = (data, type, opts = {}) => { case commonStats.carbs: if (days > 1) { - annotations.push(t('**Avg. Daily Carbs**: All carb entries added together, then divided by the number of days in this view. Note, these entries come from either bolus wizard events, or Apple Health records.')); + annotations.push(t('**Avg. Daily Carbs**: All carb entries added together, then divided by the number of days in this view for which we have carb data. Note, these entries come from either bolus wizard events, or Apple Health records.')); } else { annotations.push(t('**Total Carbs**: All carb entries from bolus wizard events or Apple Health records added together.')); } @@ -385,11 +385,11 @@ export const getStatAnnotations = (data, type, opts = {}) => { case commonStats.totalInsulin: if (days > 1) { - annotations.push(t('**Total Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view')); + annotations.push(t('**Total Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view for which we have insulin data')); } else { annotations.push(t('**Total Insulin:** All basal and bolus insulin delivery (in Units) added together')); } - annotations.push(t('**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for this time period.')); + annotations.push(t('**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for the time period for which we have insulin data.')); break; default: @@ -722,6 +722,7 @@ export const getStatData = (data, type, opts = {}) => { export const getStatTitle = (type, opts = {}) => { const { bgSource, days } = opts; const vocabulary = getPumpVocabulary(opts.manufacturer); + const bgTypeLabel = bgSource === 'cbg' ? t('Glucose') : t('BG'); let title; @@ -735,7 +736,7 @@ export const getStatTitle = (type, opts = {}) => { break; case commonStats.bgExtents: - title = t('BG Extents ({{bgSourceLabel}})', { bgSourceLabel: statBgSourceLabels[bgSource] }); + title = t('{{bgTypeLabel}} Extents ({{bgSourceLabel}})', { bgSourceLabel: statBgSourceLabels[bgSource], bgTypeLabel }); break; case commonStats.carbs: diff --git a/src/utils/validation/schema.js b/src/utils/validation/schema.js index 27452fc9c..cd19fee2f 100644 --- a/src/utils/validation/schema.js +++ b/src/utils/validation/schema.js @@ -407,6 +407,7 @@ export default { equil: v.compile(pumpSettingsEquil), 'diy loop': v.compile(pumpSettingsLoop), 'tidepool loop': v.compile(pumpSettingsLoop), + twiist: v.compile(pumpSettingsLoop), }, smbg: v.compile(bg), wizard: v.compile(wizard), diff --git a/static-assets/images/sitechange-twiist-cassette.png b/static-assets/images/sitechange-twiist-cassette.png new file mode 100644 index 000000000..bc45dc5cf Binary files /dev/null and b/static-assets/images/sitechange-twiist-cassette.png differ diff --git a/storybook/main.js b/storybook/main.js index 4556f7221..13fe96b1c 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -37,6 +37,7 @@ const config = { }, }, plugins: [...config.plugins, ...custom.plugins], + devtool: 'inline-source-map', }; return finalConfig; diff --git a/test/components/common/stat/BgBar.test.js b/test/components/common/stat/BgBar.test.js index 7225535a8..03e5e007d 100644 --- a/test/components/common/stat/BgBar.test.js +++ b/test/components/common/stat/BgBar.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { mount } from 'enzyme'; import _ from 'lodash'; +import { Rect } from 'victory-core'; import BgBar from '../../../../src/components/common/stat/BgBar'; import colors from '../../../../src/styles/colors.css'; @@ -45,8 +46,8 @@ describe('BgBar', () => { chartLabelWidth: 80, datum: avgGlucoseDatum, domain: { - x: [0, MGDL_CLAMP_TOP], - y: [0, 1], + x: [0, 1], + y: [0, MGDL_CLAMP_TOP], }, scale: { x: val => val, @@ -121,9 +122,9 @@ describe('BgBar', () => { it('should render a three-bar scale with arcs on each end', () => { expect(bgScale().children()).to.have.length(5); expect(bgScale().childAt(0).is('Arc')).to.be.true; - expect(bgScale().childAt(1).is('Rect')).to.be.true; - expect(bgScale().childAt(2).is('Rect')).to.be.true; - expect(bgScale().childAt(3).is('Rect')).to.be.true; + expect(bgScale().childAt(1).is(Rect)).to.be.true; + expect(bgScale().childAt(2).is(Rect)).to.be.true; + expect(bgScale().childAt(3).is(Rect)).to.be.true; expect(bgScale().childAt(4).is('Arc')).to.be.true; }); @@ -221,8 +222,8 @@ describe('BgBar', () => { it('should render 2 standard deviation markers', () => { expect(bgDeviation().children()).to.have.length(2); - expect(bgDeviation().childAt(0).is('Rect')).to.be.true; - expect(bgDeviation().childAt(1).is('Rect')).to.be.true; + expect(bgDeviation().childAt(0).is(Rect)).to.be.true; + expect(bgDeviation().childAt(1).is(Rect)).to.be.true; }); it('should render the deviation markers with the proper colors', () => { diff --git a/test/components/common/stat/HoverBar.test.js b/test/components/common/stat/HoverBar.test.js index 7d87fd59d..7c5db8673 100644 --- a/test/components/common/stat/HoverBar.test.js +++ b/test/components/common/stat/HoverBar.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import { Bar, Rect } from 'victory'; import HoverBar from '../../../../src/components/common/stat/HoverBar'; import colors from '../../../../src/styles/colors.css'; @@ -24,7 +25,7 @@ describe('HoverBar', () => { y: () => width, }, width, - y: 80, + x: 80, }; beforeEach(() => { @@ -37,7 +38,7 @@ describe('HoverBar', () => { it('should render a full-width transparent hover target area', () => { expect(wrapper.find('.HoverBarTarget')).to.have.length(1); - expect(wrapper.find('.HoverBarTarget').childAt(0).is('Rect')).to.be.true; + expect(wrapper.find('.HoverBarTarget').childAt(0).is(Rect)).to.be.true; expect(wrapper.find('.HoverBarTarget').childAt(0).props().style.stroke).to.equal('transparent'); expect(wrapper.find('.HoverBarTarget').childAt(0).props().style.fill).to.equal('transparent'); expect(wrapper.find('.HoverBarTarget').childAt(0).props().width).to.equal(width); @@ -45,19 +46,19 @@ describe('HoverBar', () => { it('should render a properly colored bar backround the full width of the rendering area', () => { expect(wrapper.find('.barBg')).to.have.length(1); - expect(wrapper.find('.barBg').childAt(0).is('Rect')).to.be.true; + expect(wrapper.find('.barBg').childAt(0).is(Rect)).to.be.true; expect(wrapper.find('.barBg').childAt(0).props().style.stroke).to.equal('transparent'); expect(wrapper.find('.barBg').childAt(0).props().style.fill).to.equal(colors.axis); expect(wrapper.find('.barBg').childAt(0).props().width).to.equal(220); // width - chartLabelWidth }); - it('should render a bar with a width corresponding to the y prop value, corrected for the rendering area width', () => { + it('should render a bar with a width corresponding to the x prop value, corrected for the rendering area width', () => { // actual chart rendering width is corrected due to the chart labels taking some space const widthCorrection = (width - defaultProps.chartLabelWidth) / width; expect(widthCorrection).to.equal(0.7333333333333333); // (220 / 300), as per default props - expect(wrapper.find('Bar')).to.have.length(1); - expect(wrapper.find('Bar').props().width).to.equal(220); - expect(wrapper.find('Bar').props().y).to.equal(58.666666666666664); // (defaultProps.y * widthCorrection) + expect(wrapper.find(Bar)).to.have.length(1); + expect(wrapper.find(Bar).props().width).to.equal(220); + expect(wrapper.find(Bar).props().x).to.equal(58.666666666666664); // (defaultProps.x * widthCorrection) }); }); diff --git a/test/components/common/stat/Stat.test.js b/test/components/common/stat/Stat.test.js index 196bf6b29..99d829549 100644 --- a/test/components/common/stat/Stat.test.js +++ b/test/components/common/stat/Stat.test.js @@ -1482,10 +1482,10 @@ describe('Stat', () => { it('should set `containerComponent` to a non-responsive `VictoryContainer` component', () => { const result = instance.getChartPropsByType(instance.props); - const containerComponentInstance = shallow(result.containerComponent).instance(); + const containerComponent = mount(result.containerComponent); - expect(containerComponentInstance).to.be.instanceOf(VictoryContainer); - expect(containerComponentInstance.props.responsive).to.be.false; + expect(containerComponent.is(VictoryContainer)).to.be.true; + expect(containerComponent.prop('responsive')).to.be.false; }); it('should set `dataComponent` to a `BgBar` component with necessary props', () => { @@ -1544,8 +1544,8 @@ describe('Stat', () => { const result = instance.getChartPropsByType(instance.props); expect(result.domain).to.eql({ - x: [0, MGDL_CLAMP_TOP], - y: [0, 1], + x: [0, 1], + y: [0, MGDL_CLAMP_TOP], }); }); @@ -1557,8 +1557,8 @@ describe('Stat', () => { const result = instance.getChartPropsByType(instance.props); expect(result.domain).to.eql({ - x: [0, MMOLL_CLAMP_TOP], - y: [0, 1], + x: [0, 1], + y: [0, MMOLL_CLAMP_TOP], }); }); @@ -1657,10 +1657,10 @@ describe('Stat', () => { it('should set `containerComponent` to a non-responsive `VictoryContainer` component', () => { const result = instance.getChartPropsByType(instance.props); - const containerComponentInstance = shallow(result.containerComponent).instance(); + const containerComponent = mount(result.containerComponent); - expect(containerComponentInstance).to.be.instanceOf(VictoryContainer); - expect(containerComponentInstance.props.responsive).to.be.false; + expect(containerComponent.is(VictoryContainer)).to.be.true; + expect(containerComponent.prop('responsive')).to.be.false; }); it('should set `dataComponent` to a `HoverBar` component with necessary props', () => { @@ -1718,8 +1718,8 @@ describe('Stat', () => { const result = instance.getChartPropsByType(instance.props); expect(result.domain).to.eql({ - x: [0, 1], - y: [0, 2], + x: [0, 2], + y: [0, 1], }); // Remove a datum diff --git a/test/components/settings/common/Header.test.js b/test/components/settings/common/Header.test.js index 105e7177a..f7ed8bb03 100644 --- a/test/components/settings/common/Header.test.js +++ b/test/components/settings/common/Header.test.js @@ -29,6 +29,6 @@ describe('Header', () => { printView={false} /> ); - expect(wrapper.find('span').text()).to.equal('Therapy Settings - Active at Upload on Jul 12th 2016'); + expect(wrapper.find('span').text()).to.equal('Active at Upload on Jul 12th 2016'); }); }); diff --git a/test/modules/print/BasicsPrintView.test.js b/test/modules/print/BasicsPrintView.test.js index aeb9ac022..e46ddc994 100644 --- a/test/modules/print/BasicsPrintView.test.js +++ b/test/modules/print/BasicsPrintView.test.js @@ -232,7 +232,7 @@ describe('BasicsPrintView', () => { sinon.assert.calledWithMatch(Renderer.renderCalendarSection, { title: { text: Renderer.sections.boluses.title, - subText: '(days with no boluses have been excluded)', + subText: '(days with no insulin data have been excluded)', }, data: Renderer.aggregationsByDate.boluses.byDate, type: 'bolus', @@ -362,7 +362,7 @@ describe('BasicsPrintView', () => { 'timeInRangeStub', { heading: { - text: 'BG Distribution', + text: 'Time in Range', note: 'Showing CGM data', }, secondaryFormatKey: 'tooltip', @@ -382,7 +382,7 @@ describe('BasicsPrintView', () => { { data: { raw: { counts: { total: 11 } } } }, { heading: { - text: 'BG Distribution', + text: 'Readings in Range', note: 'BGM data from 11 readings', }, secondaryFormatKey: 'tooltip', diff --git a/test/modules/print/DailyPrintView.test.js b/test/modules/print/DailyPrintView.test.js index 9d9866dd6..e0bb3fbf9 100644 --- a/test/modules/print/DailyPrintView.test.js +++ b/test/modules/print/DailyPrintView.test.js @@ -436,7 +436,7 @@ describe('DailyPrintView', () => { }); it('should render the Average BG stat if available', () => { - sinon.assert.calledWith(Renderer.doc.text, 'Average BG'); + sinon.assert.calledWith(Renderer.doc.text, 'Avg Glucose'); sinon.assert.calledWith(Renderer.doc.text, '120 mg/dL'); }); @@ -482,7 +482,7 @@ describe('DailyPrintView', () => { }); it('should render the Average BG in mmol/L with correct formatting', () => { - sinon.assert.calledWith(Renderer.doc.text, 'Average BG'); + sinon.assert.calledWith(Renderer.doc.text, 'Avg Glucose'); sinon.assert.calledWith(Renderer.doc.text, '12.3 mmol/L'); }); }); diff --git a/test/utils/DataUtil.test.js b/test/utils/DataUtil.test.js index 936dc0f12..2e07aafd8 100644 --- a/test/utils/DataUtil.test.js +++ b/test/utils/DataUtil.test.js @@ -96,12 +96,14 @@ describe('DataUtil', () => { }), new Types.CBG({ deviceId: 'Dexcom-XXX-XXXX', + origin: { name: 'Dexcom G6', version: '2.3.2' }, value: 190, deviceTime: '2018-02-01T00:45:00', ...useRawData, }), new Types.CBG({ deviceId: 'Dexcom-XXX-XXXX', + origin: { name: 'Dexcom G6', version: '3.1.0' }, value: 260, deviceTime: '2018-02-01T00:50:00', ...useRawData, @@ -259,12 +261,14 @@ describe('DataUtil', () => { dataSetType: 'continuous', deviceTime: '2018-02-03T00:00:00', uploadId: 'upload-3', + client: { name: 'org.tidepool.Loop' }, ...useRawData, }), new Types.Upload({ dataSetType: 'continuous', deviceTime: '2018-02-04T00:00:00', uploadId: 'upload-4', + client: { name: 'com.loopkit.Loop' }, ...useRawData, }), ], _.toPlainObject); @@ -434,6 +438,29 @@ describe('DataUtil', () => { expect(dataUtil.wizardToBolusIdMap[newWizard.id]).to.equal(newBolus.id); }); + it('should create and/or update the `loopDataSetsByIdMap`', () => { + delete dataUtil.loopDataSetsByIdMap; + expect(dataUtil.loopDataSetsByIdMap).to.be.undefined; + + dataUtil.addData(defaultData, defaultPatientId); + + expect(dataUtil.loopDataSetsByIdMap).to.be.an('object').and.have.keys([ + uploadData[3].id, + uploadData[4].id, + ]); + + const newUpload = new Types.Upload({ ...useRawData, dataSetType: 'continuous', client: { name: 'com.loopkit.Loop' } }); + dataUtil.addData([newUpload], defaultPatientId); + + expect(dataUtil.loopDataSetsByIdMap).to.be.an('object').and.have.keys([ + uploadData[3].id, + uploadData[4].id, + newUpload.id, + ]); + + expect(dataUtil.loopDataSetsByIdMap[newUpload.id].id).to.equal(newUpload.id); + }); + it('should create and/or update the `bolusDatumsByIdMap`', () => { delete dataUtil.bolusDatumsByIdMap; expect(dataUtil.bolusDatumsByIdMap).to.be.undefined; @@ -952,7 +979,9 @@ describe('DataUtil', () => { describe('joinBolusAndDosingDecision', () => { it('should join loop dosing decisions, and associated pump settings, to boluses that are within a minute of each other', () => { - const bolus = { type: 'bolus', id: 'bolus1', time: Date.parse('2024-02-02T10:05:59.000Z'), origin: { name: 'org.tidepool.Loop' } }; + const uploadId = 'upload1'; + const upload = { type: 'upload', id: uploadId, dataSetType: 'continuous', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } }; + const bolus = { type: 'bolus', id: 'bolus1', uploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), origin: { name: 'org.tidepool.Loop' } }; const pumpSettings = { ...loopMultirate, id: 'pumpSettings1' }; const dosingDecision = { @@ -972,6 +1001,7 @@ describe('DataUtil', () => { dataUtil.bolusDosingDecisionDatumsByIdMap = { dosingDecision1: dosingDecision }; dataUtil.pumpSettingsDatumsByIdMap = { pumpSettings1: pumpSettings }; + dataUtil.loopDataSetsByIdMap = { [uploadId]: upload }; dataUtil.joinBolusAndDosingDecision(bolus); // should attach associated pump settings to dosingDecisions @@ -1206,6 +1236,17 @@ describe('DataUtil', () => { expect(dosingDecisionBolus.tags.underride).to.be.false; expect(dosingDecisionBolus.tags.wizard).to.be.true; }); + + it('should tag a loop bolus with `loop`', () => { + const loopUploadId = 'upload1'; + const loopUpload = { type: 'upload', id: loopUploadId, dataSetType: 'continuous', uploadId: loopUploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } }; + dataUtil.loopDataSetsByIdMap = { [loopUploadId]: loopUpload }; + const loopBolus = { ...bolus, deviceTime: '2018-02-02T01:00:00', uploadId: loopUploadId }; + + expect(loopBolus.tags).to.be.undefined; + dataUtil.tagDatum(loopBolus); + expect(loopBolus.tags.loop).to.be.true; + }); }); context('smbg', () => { @@ -1228,6 +1269,19 @@ describe('DataUtil', () => { }); }); + context('food', () => { + it('should tag a loop food datum with `loop`', () => { + const loopUploadId = 'upload1'; + const loopUpload = { type: 'upload', id: loopUploadId, dataSetType: 'continuous', uploadId: loopUploadId, time: Date.parse('2024-02-02T10:05:59.000Z'), client: { name: 'org.tidepool.Loop' } }; + dataUtil.loopDataSetsByIdMap = { [loopUploadId]: loopUpload }; + const loopFood = new Types.Food({ deviceTime: '2018-02-01T01:00:00', uploadId: loopUploadId, ...useRawData }); + + expect(loopFood.tags).to.be.undefined; + dataUtil.tagDatum(loopFood); + expect(loopFood.tags.loop).to.be.true; + }); + }); + context('deviceEvent', () => { const calibration = new Types.DeviceEvent({ deviceTime: '2018-02-01T01:00:00', subType: 'calibration', ...useRawData }); const siteChange = new Types.DeviceEvent({ deviceTime: '2018-02-01T01:00:00', ...useRawData }); @@ -3068,6 +3122,59 @@ describe('DataUtil', () => { expect(dataUtil.excludedDevices).to.eql(['tandem12345']); }); + + it('should add set the proper device label for LibreView data', () => { + initDataUtil([{ + ...uploadData[3], + deviceManufacturers: ['Abbott'], + deviceId: 'MyAbbott123', + dataSetType: 'continuous', + deviceTags: [ + 'bgm', + 'cgm' + ], + }]); + + delete(dataUtil.devices); + dataUtil.setDevices(); + + expect(dataUtil.devices).to.eql([ + { + bgm: true, + cgm: true, + id: 'MyAbbott123', + label: 'FreeStyle Libre (from LibreView)', + pump: false, + serialNumber: undefined + }, + ]); + }); + + it('should add set the proper device label for Dexcom API data', () => { + initDataUtil([{ + ...uploadData[3], + deviceManufacturers: ['Dexcom'], + deviceId: 'MyDexcom123', + dataSetType: 'continuous', + deviceTags: [ + 'cgm' + ], + }]); + + delete(dataUtil.devices); + dataUtil.setDevices(); + + expect(dataUtil.devices).to.eql([ + { + bgm: false, + cgm: true, + id: 'MyDexcom123', + label: 'Dexcom API', + pump: false, + serialNumber: undefined + }, + ]); + }); }); describe('setMetaData', () => { @@ -4018,9 +4125,9 @@ describe('DataUtil', () => { dataUtil.getStats(['averageGlucose', 'totalInsulin']); expect(dataUtil.matchedDevices).to.eql({ - 'Dexcom-XXX-XXXX': true, - 'AbbottFreeStyleLibre-XXX-XXXX': true, - 'Test Page Data - 123': true, + 'Dexcom-XXX-XXXX': { 'Dexcom G6_2.3.2': true, 'Dexcom G6_3.1.0': true }, + 'AbbottFreeStyleLibre-XXX-XXXX': { 'AbbottFreeStyleLibre-XXX-XXXX_0.0': true }, + 'Test Page Data - 123': { 'Test Page Data - 123_0.0': true }, }); dataUtil.setBgSources('smbg'); @@ -4029,7 +4136,7 @@ describe('DataUtil', () => { dataUtil.getStats(['averageGlucose']); expect(dataUtil.matchedDevices).to.eql({ - 'OneTouch-XXX-XXXX': true, + 'OneTouch-XXX-XXXX': { 'OneTouch-XXX-XXXX_0.0': true }, }); // Should not update if `matchDevices` is false @@ -4076,21 +4183,21 @@ describe('DataUtil', () => { dataUtil.getAggregationsByDate(['basals']); expect(dataUtil.matchedDevices).to.eql({ - 'Test Page Data - 123': true, + 'Test Page Data - 123': { 'Test Page Data - 123_0.0': true }, }); dataUtil.clearMatchedDevices(); dataUtil.getAggregationsByDate(['fingersticks']); expect(dataUtil.matchedDevices).to.eql({ - 'OneTouch-XXX-XXXX': true, + 'OneTouch-XXX-XXXX': { 'OneTouch-XXX-XXXX_0.0': true }, }); dataUtil.clearMatchedDevices(); dataUtil.getAggregationsByDate(['boluses']); expect(dataUtil.matchedDevices).to.eql({ - 'Test Page Data - 123': true, + 'Test Page Data - 123': { 'Test Page Data - 123_0.0': true }, }); // Should not update if `matchDevices` is false @@ -4567,9 +4674,8 @@ describe('DataUtil', () => { dataUtil.matchDevices = true; dataUtil.setTypes({ bolus: {} }); dataUtil.getTypeData(dataUtil.types); - expect(dataUtil.matchedDevices).to.eql({ - 'Test Page Data - 123': true, + 'Test Page Data - 123': { 'Test Page Data - 123_0.0': true }, }); dataUtil.clearMatchedDevices(); @@ -4577,7 +4683,7 @@ describe('DataUtil', () => { dataUtil.getTypeData(dataUtil.types); expect(dataUtil.matchedDevices).to.eql({ - 'OneTouch-XXX-XXXX': true, + 'OneTouch-XXX-XXXX': { 'OneTouch-XXX-XXXX_0.0': true }, }); dataUtil.clearMatchedDevices(); @@ -4585,7 +4691,7 @@ describe('DataUtil', () => { dataUtil.getTypeData(dataUtil.types); expect(dataUtil.matchedDevices).to.eql({ - 'Test Page Data - 123': true, + 'Test Page Data - 123': { 'Test Page Data - 123_0.0': true }, }); dataUtil.clearMatchedDevices(); @@ -4593,8 +4699,8 @@ describe('DataUtil', () => { dataUtil.getTypeData(dataUtil.types); expect(dataUtil.matchedDevices).to.eql({ - 'Dexcom-XXX-XXXX': true, - 'AbbottFreeStyleLibre-XXX-XXXX': true, + 'Dexcom-XXX-XXXX': { 'Dexcom G6_2.3.2': true, 'Dexcom G6_3.1.0': true }, + 'AbbottFreeStyleLibre-XXX-XXXX': { 'AbbottFreeStyleLibre-XXX-XXXX_0.0': true }, }); // Should not update if `matchDevices` is false diff --git a/test/utils/StatUtil.test.js b/test/utils/StatUtil.test.js index aa7564c2f..59e7ef41f 100644 --- a/test/utils/StatUtil.test.js +++ b/test/utils/StatUtil.test.js @@ -423,6 +423,15 @@ describe('StatUtil', () => { }); }); + it('calculates insulin delivery using only data-populated days when viewing more than 1 day', () => { + filterEndpoints(twoDayEndpoints); + statUtil.activeDays = 7; // data only exists for 2 days; this should not impact the calculation + expect(statUtil.getBasalBolusData()).to.eql({ + basal: 0.75, + bolus: 7.5, + }); + }); + context('basal delivery overlaps endpoints', () => { it('should include the portion of delivery of a basal datum that overlaps the start endpoint', () => { statUtil.dataUtil.addData([basalDatumOverlappingStart], patientId); @@ -482,6 +491,15 @@ describe('StatUtil', () => { total: 7, }); }); + + it('calculates avg daily carbs using only data-populated days when viewing more than 1 day', () => { + filterEndpoints(twoDayEndpoints); + statUtil.activeDays = 7; // data only exists for 2 days; this should not impact the calculation + expect(statUtil.getCarbsData()).to.eql({ + carbs: { grams: 21.5, exchanges: 1 }, + total: 7, + }); + }); }); describe('getCoefficientOfVariationData', () => { diff --git a/test/utils/agp/data.test.js b/test/utils/agp/data.test.js new file mode 100644 index 000000000..9ee638f00 --- /dev/null +++ b/test/utils/agp/data.test.js @@ -0,0 +1,171 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2016, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +import * as utils from '../../../src/utils/agp/data'; +import { formatCurrentDate } from '../../../src/utils/datetime'; + +const { agpCGMText } = utils; + +const patient = { + birthDate: '2001-01-01', + email: 'tcrawford@test.test', + fullName: 'Terence Crawford', + id: '1234-abcd', +}; + +const data = { + data: { + current: { + aggregationsByDate: {}, + stats: { + averageGlucose: { + averageGlucose: 121.4013071783735, + total: 8442, + }, + bgExtents: { + bgMax: 401.00001001500004, + bgMin: 38.9999690761, + bgDaysWorn: 30, + newestDatum: { time: 1736783190269 }, + oldestDatum: { time: 1734249705225 }, + }, + coefficientOfVariation: { + coefficientOfVariation: 49.82047988955789, + total: 8442, + }, + glucoseManagementIndicator: { + glucoseManagementIndicator: 6.236871267706695, + glucoseManagementIndicatorAGP: 6.236871267706695, + total: 8442, + }, + sensorUsage: { + sensorUsage: 2532600000, + sensorUsageAGP: 99.95264030310206, + total: 2592000000, + sampleFrequency: 300000, + count: 8442, + }, + timeInRange: { + durations: { + veryLow: 337739.8720682303, + low: 2507462.686567164, + target: 66023027.7185501, + high: 14031556.503198294, + veryHigh: 3500213.219616205, + total: 2532600000, + }, + counts: { + veryLow: 33, + low: 245, + target: 6451, + high: 1371, + veryHigh: 342, + total: 8442, + }, + }, + }, + endpoints: { + range: [1734249600000, 1736841600000], + days: 30, + activeDays: 30, + }, + data: {}, + }, + }, + timePrefs: { timezoneAware: true, timezoneName: 'Etc/GMT+8' }, + bgPrefs: { + bgBounds: { + veryHighThreshold: 250, + targetUpperBound: 180, + targetLowerBound: 70, + veryLowThreshold: 54, + extremeHighThreshold: 350, + clampThreshold: 600, + }, + bgClasses: { + low: { boundary: 70 }, + target: { boundary: 180 }, + 'very-low': { boundary: 54 }, + high: { boundary: 250 }, + }, + bgUnits: 'mg/dL', + }, + query: { + endpoints: [ + 1734249600000, + 1736841600000, + ], + aggregationsByDate: 'dataByDate, statsByDate', + bgSource: 'cbg', + stats: [ + 'averageGlucose', + 'bgExtents', + 'coefficientOfVariation', + 'glucoseManagementIndicator', + 'sensorUsage', + 'timeInRange', + ], + types: { cbg: {} }, + bgPrefs: { + bgUnits: 'mg/dL', + bgClasses: { + low: { boundary: 70 }, + target: { boundary: 180 }, + 'very-low': { boundary: 54 }, + high: { boundary: 250 }, + }, + bgBounds: { + veryHighThreshold: 250, + targetUpperBound: 180, + targetLowerBound: 70, + veryLowThreshold: 54, + extremeHighThreshold: 350, + clampThreshold: 600, + }, + }, + metaData: 'latestPumpUpload, bgSources', + timePrefs: { + timezoneAware: true, + timezoneName: 'Etc/GMT+8', + }, + excludedDevices: [], + }, + metaData: {}, +}; + +const expectedOutput = ( +`Terence Crawford +Date of birth: 2001-01-01 +Exported from Tidepool TIDE: ${formatCurrentDate()} + +Reporting Period: December 15, 2024 - January 13, 2025 + +Avg. Daily Time In Range (mg/dL) +70-180 76% (18h 20m) +54-70 3% (42m) +<54 0% (6m) + +Avg. Glucose (CGM): 121 mg/dL +`); + +describe('[agp] data utils', () => { + describe('agpCGMText', () => { + it('should return the expected output', () => { + expect(agpCGMText(patient, data)).to.eql(expectedOutput); + }); + }); +}); diff --git a/test/utils/basal.test.js b/test/utils/basal.test.js index fa6eee76d..cb647a546 100644 --- a/test/utils/basal.test.js +++ b/test/utils/basal.test.js @@ -444,6 +444,22 @@ describe('basal utilties', () => { '2018-01-02T05:00:00.000Z', ]; expect(basalUtils.getTotalBasalFromEndpoints(data, endpoints)).to.equal('10.25'); + + // basal completely encompasses endpoints + endpoints = [ + '2018-01-01T05:00:00.000Z', + '2018-01-02T05:00:00.000Z', + ]; + + const multiDayBasalData = [ + { // 72 U total delivery, but endpoints are only 24h + duration: MS_IN_HOUR * 72, + rate: 1, + normalTime: '2017-12-31T05:00:00.000Z', + normalEnd: '2018-01-03T05:00:00.000Z', + }, + ]; + expect(basalUtils.getTotalBasalFromEndpoints(multiDayBasalData, endpoints)).to.equal('24.0'); }); }); @@ -465,7 +481,7 @@ describe('basal utilties', () => { subType: 'automated', }, { - duration: MS_IN_HOUR * 7, + duration: MS_IN_HOUR * 19, rate: 0.5, normalTime: '2018-01-01T05:00:00.000Z', normalEnd: '2018-01-02T00:00:00.000Z', @@ -487,20 +503,19 @@ describe('basal utilties', () => { ]; expect(basalUtils.getBasalGroupDurationsFromEndpoints(data, endpoints)).to.eql({ automated: MS_IN_HOUR * 2, - manual: MS_IN_HOUR * 10, + manual: MS_IN_HOUR * 22, }); - // endpoints shifted to an hour after basal delivery begins + // endpoints shifted to an hour after basal delivery ends endpoints = [ - '2018-01-01T01:00:00.000Z', - '2018-01-02T01:00:00.000Z', + '2018-01-01T04:00:00.000Z', + '2018-01-02T04:00:00.000Z', ]; expect(basalUtils.getBasalGroupDurationsFromEndpoints(data, endpoints)).to.eql({ - automated: MS_IN_HOUR * 2, - manual: MS_IN_HOUR * 10, + automated: MS_IN_HOUR * 1, + manual: MS_IN_HOUR * 22, }); - // endpoints shifted to an hour before basal delivery begins endpoints = [ '2017-12-31T23:00:00.000Z', @@ -508,7 +523,7 @@ describe('basal utilties', () => { ]; expect(basalUtils.getBasalGroupDurationsFromEndpoints(data, endpoints)).to.eql({ automated: MS_IN_HOUR * 2, - manual: MS_IN_HOUR * 9, + manual: MS_IN_HOUR * 21, }); // endpoints shifted to two hours after basal delivery ends @@ -517,8 +532,28 @@ describe('basal utilties', () => { '2018-01-02T05:00:00.000Z', ]; expect(basalUtils.getBasalGroupDurationsFromEndpoints(data, endpoints)).to.eql({ - automated: MS_IN_HOUR * 2, - manual: MS_IN_HOUR * 8, + automated: 0, + manual: MS_IN_HOUR * 22, + }); + + // basal completely encompasses endpoints + endpoints = [ + '2018-01-01T05:00:00.000Z', + '2018-01-02T05:00:00.000Z', + ]; + + const multiDayBasalData = [ + { // 72 U total delivery, but endpoints are only 24h + duration: MS_IN_HOUR * 72, + rate: 1, + normalTime: '2017-12-31T05:00:00.000Z', + normalEnd: '2018-01-03T05:00:00.000Z', + subType: 'automated', + }, + ]; + expect(basalUtils.getBasalGroupDurationsFromEndpoints(multiDayBasalData, endpoints)).to.eql({ + automated: MS_IN_HOUR * 24, + manual: 0, }); }); }); diff --git a/test/utils/basics/data.test.js b/test/utils/basics/data.test.js index cc7f053de..c92804d24 100644 --- a/test/utils/basics/data.test.js +++ b/test/utils/basics/data.test.js @@ -238,17 +238,17 @@ describe('basics data utils', () => { }); context('manufacturers: tandem, medtronic, animas', () => { - it('should return `undeclared` as the site change source if not stored in patient settings', () => { - expect(dataUtils.getSiteChangeSource(patient, 'tandem')).to.equal('undeclared'); - expect(dataUtils.getSiteChangeSource(patient, 'medtronic')).to.equal('undeclared'); - expect(dataUtils.getSiteChangeSource(patient, 'animas')).to.equal('undeclared'); + it('should return `cannulaPrime` as the site change source if not stored in patient settings', () => { + expect(dataUtils.getSiteChangeSource(patient, 'tandem')).to.equal('cannulaPrime'); + expect(dataUtils.getSiteChangeSource(patient, 'medtronic')).to.equal('cannulaPrime'); + expect(dataUtils.getSiteChangeSource(patient, 'animas')).to.equal('cannulaPrime'); }); - it('should return `undeclared` as the site change source value stored in patient settings is not in allowed list', () => { + it('should return `cannulaPrime` as the site change source value stored in patient settings is not in allowed list', () => { const patientWithBadSiteChangeSource = { ...patient, settings: { siteChangeSource: 'foo' } }; - expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'tandem')).to.equal('undeclared'); - expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'medtronic')).to.equal('undeclared'); - expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'animas')).to.equal('undeclared'); + expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'tandem')).to.equal('cannulaPrime'); + expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'medtronic')).to.equal('cannulaPrime'); + expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'animas')).to.equal('cannulaPrime'); }); it('should return the site change source value stored in patient settings if it is in allowed list', () => { @@ -264,38 +264,57 @@ describe('basics data utils', () => { expect(dataUtils.getSiteChangeSource(patientWithTubingPrime, 'animas')).to.equal('tubingPrime'); }); }); + + context('manufacturers: twiist', () => { + it('should return `reservoirChange` as the site change source if not stored in patient settings', () => { + expect(dataUtils.getSiteChangeSource(patient, 'twiist')).to.equal('reservoirChange'); + }); + + it('should return `reservoirChange` as the site change source value stored in patient settings is not in allowed list', () => { + const patientWithBadSiteChangeSource = { ...patient, settings: { siteChangeSource: 'foo' } }; + expect(dataUtils.getSiteChangeSource(patientWithBadSiteChangeSource, 'twiist')).to.equal('reservoirChange'); + }); + + it('should return the site change source value stored in patient settings if it is in allowed list', () => { + const patientWithReservoirChange = { ...patient, settings: { siteChangeSource: 'reservoirChange' } }; + const patientWithCannulaPrime = { ...patient, settings: { siteChangeSource: 'cannulaPrime' } }; + + expect(dataUtils.getSiteChangeSource(patientWithReservoirChange, 'twiist')).to.equal('reservoirChange'); + expect(dataUtils.getSiteChangeSource(patientWithCannulaPrime, 'twiist')).to.equal('cannulaPrime'); + }); + }); }); describe('getSiteChangeSourceLabel', () => { it('should return the appropriate labels for animas pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'animas')).to.equal('Fill Cannula'); + expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'animas')).to.equal('Cannula Fill'); expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'animas')).to.equal('Go Prime'); }); it('should return the appropriate labels for medtronic pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'medtronic')).to.equal('Prime Cannula'); + expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'medtronic')).to.equal('Cannula Prime'); expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'medtronic')).to.equal('Prime'); }); it('should return the appropriate labels for tandem pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'tandem')).to.equal('Fill Cannula'); - expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'tandem')).to.equal('Fill Tubing'); + expect(dataUtils.getSiteChangeSourceLabel('cannulaPrime', 'tandem')).to.equal('Cannula Fill'); + expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'tandem')).to.equal('Tubing Fill'); }); it('should return the appropriate labels for insulet pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('reservoirChange', 'insulet')).to.equal('Change Pod'); + expect(dataUtils.getSiteChangeSourceLabel('reservoirChange', 'insulet')).to.equal('Pod Change'); }); it('should return the appropriate labels for DIY Loop pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'diy loop')).to.equal('Fill Tubing'); + expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'diy loop')).to.equal('Tubing Fill'); }); it('should return the appropriate labels for Tidepool Loop pumps', () => { - expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'tidepool loop')).to.equal('Fill Tubing'); + expect(dataUtils.getSiteChangeSourceLabel('tubingPrime', 'tidepool loop')).to.equal('Tubing Fill'); }); it('should fall back to a default label when a manufacturer-specific label cannot be found', () => { - expect(dataUtils.getSiteChangeSourceLabel('myEvent', 'pumpCo')).to.equal('Change Cartridge'); + expect(dataUtils.getSiteChangeSourceLabel('myEvent', 'pumpCo')).to.equal('Cartridge Change'); }); it('should return `null` when siteChangeSource is `undeclared`', () => { @@ -385,10 +404,6 @@ describe('basics data utils', () => { // siteChanges gets emptyText set when no data expect(resultWithMissingData.siteChanges.emptyText).to.equal("This section requires data from an insulin pump, so there's nothing to display."); - - // siteChanges gets emptyText set when data present but no siteChangeSource selected - const resultWithSiteChangeDataNoSource = dataUtils.processBasicsAggregations({ ...aggregations }, { ...data, siteChanges: { byDate: { '2019-12-02': 'data' } } }, patient, manufacturer); - expect(resultWithSiteChangeDataNoSource.siteChanges.emptyText).to.equal("Please choose a preferred site change source from the 'Basics' web view to view this data."); }); }); @@ -589,7 +604,6 @@ describe('basics data utils', () => { }, bgPrefs: bgPrefs[MGDL_UNITS], timePrefs, - query: { excludeDaysWithoutBolus: true }, metaData: { devices: [ { id: 'deviceWithLabelId', label: 'Device With Label' }, @@ -645,8 +659,8 @@ describe('basics data utils', () => { }, siteChanges: { type: 'siteChanges', - title: 'Infusion site changes', - subTitle: 'Fill Cannula', + title: 'Site Changes', + subTitle: 'Cannula Fill', source: 'cannulaPrime', }, }; @@ -691,7 +705,7 @@ describe('basics data utils', () => { it('should add a note regarding excluded basics bolus days', () => { dataUtils.basicsText(patient, data, stats, aggregations); - sinon.assert.calledWith(textUtilStub.buildTextLine, 'Days with no boluses have been excluded from bolus calculations'); + sinon.assert.calledWith(textUtilStub.buildTextLine, 'Days with no insulin data have been excluded from calculations'); }); it('should build the basics stats section', () => { @@ -724,7 +738,7 @@ describe('basics data utils', () => { dataUtils.basicsText(patient, data, stats, aggregations); sinon.assert.calledWith( textUtilStub.buildTextTable, - 'Infusion site changes from \'Fill Cannula\'', + 'Site Changes from \'Cannula Fill\'', [{ label: 'Mean Duration', value: '2 days' }, { label: 'Longest Duration', value: '3 days' }], [{ key: 'label', label: 'Label' }, { key: 'value', label: 'Value' }], { showHeader: false } ); @@ -745,7 +759,7 @@ describe('basics data utils', () => { dataUtils.basicsText(patient, updatedData, stats, aggregations); sinon.assert.calledWith( textUtilStub.buildTextTable, - 'Infusion site changes from \'Fill Cannula\'', + 'Site Changes from \'Cannula Fill\'', [{ label: 'Mean Duration', value: '4.3 days' }, { label: 'Longest Duration', value: '5 days' }], [{ key: 'label', label: 'Label' }, { key: 'value', label: 'Value' }], { showHeader: false } ); diff --git a/test/utils/bloodglucose.test.js b/test/utils/bloodglucose.test.js index 2b183262c..6ae97d43b 100644 --- a/test/utils/bloodglucose.test.js +++ b/test/utils/bloodglucose.test.js @@ -507,8 +507,7 @@ describe('blood glucose utilities', () => { expect(bgUtils.cgmSampleFrequency(libre3Datum)).to.equal(5 * MS_IN_MIN); const libre2CIQDatum = { - deviceId: 'tandemCIQ_XXXXX', - payload: { fsl2: true }, + sampleInterval: MS_IN_MIN, }; const g7CIQDatum = { diff --git a/test/utils/constants.test.js b/test/utils/constants.test.js index a18f2129b..9997e6887 100644 --- a/test/utils/constants.test.js +++ b/test/utils/constants.test.js @@ -296,11 +296,11 @@ describe('constants', () => { [constants.ANIMAS]: { [constants.SITE_CHANGE_RESERVOIR]: 'Go Rewind', [constants.SITE_CHANGE_TUBING]: 'Go Prime', - [constants.SITE_CHANGE_CANNULA]: 'Fill Cannula', + [constants.SITE_CHANGE_CANNULA]: 'Cannula Fill', }, [constants.INSULET]: { - [constants.SITE_CHANGE_RESERVOIR]: 'Change Pod', - [constants.SITE_CHANGE_TUBING]: 'Activate Pod', + [constants.SITE_CHANGE_RESERVOIR]: 'Pod Change', + [constants.SITE_CHANGE_TUBING]: 'Pod Activate', [constants.SITE_CHANGE_CANNULA]: 'Prime', [constants.MAX_BOLUS]: 'Maximum Bolus', [constants.MAX_BASAL]: 'Max Basal Rate', @@ -309,7 +309,7 @@ describe('constants', () => { [constants.MEDTRONIC]: { [constants.SITE_CHANGE_RESERVOIR]: 'Rewind', [constants.SITE_CHANGE_TUBING]: 'Prime', - [constants.SITE_CHANGE_CANNULA]: 'Prime Cannula', + [constants.SITE_CHANGE_CANNULA]: 'Cannula Prime', [constants.AUTOMATED_DELIVERY]: 'Auto Mode', [constants.SCHEDULED_DELIVERY]: 'Manual', [constants.MAX_BOLUS]: 'Max Bolus', @@ -317,9 +317,9 @@ describe('constants', () => { [constants.INSULIN_DURATION]: 'Active Insulin Time', }, [constants.TANDEM]: { - [constants.SITE_CHANGE_RESERVOIR]: 'Change Cartridge', - [constants.SITE_CHANGE_TUBING]: 'Fill Tubing', - [constants.SITE_CHANGE_CANNULA]: 'Fill Cannula', + [constants.SITE_CHANGE_RESERVOIR]: 'Cartridge Change', + [constants.SITE_CHANGE_TUBING]: 'Tubing Fill', + [constants.SITE_CHANGE_CANNULA]: 'Cannula Fill', [constants.AUTOMATED_DELIVERY]: 'Automation', [constants.SCHEDULED_DELIVERY]: 'Manual', [constants.SETTINGS_OVERRIDE]: 'Activity', @@ -337,6 +337,16 @@ describe('constants', () => { [constants.MAX_BOLUS]: 'Maximum Bolus', [constants.MAX_BASAL]: 'Maximum Basal Rate', }, + [constants.TWIIST_LOOP]: { + [constants.SITE_CHANGE_RESERVOIR]: 'Cassette Change', + [constants.AUTOMATED_DELIVERY]: 'Automation', + [constants.AUTOMATED_MODE_EXITED]: 'Off', + [constants.SCHEDULED_DELIVERY]: 'Manual', + [constants.SETTINGS_OVERRIDE]: 'Preset', + [constants.PHYSICAL_ACTIVITY]: { label: 'Workout', marker: 'W' }, + [constants.MAX_BOLUS]: 'Maximum Bolus', + [constants.MAX_BASAL]: 'Maximum Basal Rate', + }, [constants.DIY_LOOP]: { [constants.AUTOMATED_DELIVERY]: 'Automation', [constants.AUTOMATED_MODE_EXITED]: 'Off', @@ -348,13 +358,13 @@ describe('constants', () => { }, [constants.MICROTECH]: { [constants.SITE_CHANGE_RESERVOIR]: 'Rewind', - [constants.SITE_CHANGE_TUBING]: 'Prime Reservoir', - [constants.SITE_CHANGE_CANNULA]: 'Prime Cannula', + [constants.SITE_CHANGE_TUBING]: 'Reservoir Prime', + [constants.SITE_CHANGE_CANNULA]: 'Cannula Prime', }, default: { - [constants.SITE_CHANGE_RESERVOIR]: 'Change Cartridge', - [constants.SITE_CHANGE_TUBING]: 'Fill Tubing', - [constants.SITE_CHANGE_CANNULA]: 'Fill Cannula', + [constants.SITE_CHANGE_RESERVOIR]: 'Cartridge Change', + [constants.SITE_CHANGE_TUBING]: 'Tubing Fill', + [constants.SITE_CHANGE_CANNULA]: 'Cannula Fill', [constants.AUTOMATED_DELIVERY]: 'Automated', [constants.AUTOMATED_SUSPEND]: 'Automated Suspend', [constants.AUTOMATED_MODE_EXITED]: 'Exited', @@ -371,6 +381,33 @@ describe('constants', () => { }); }); + describe('settingsOverrides', () => { + it('should define settings overrides per device manufacturer', () => { + expect(constants.settingsOverrides).to.eql({ + [constants.TANDEM]: [ + constants.SLEEP, + constants.PHYSICAL_ACTIVITY, + ], + [constants.TIDEPOOL_LOOP]: [ + constants.PHYSICAL_ACTIVITY, + constants.PREPRANDIAL, + ], + [constants.TWIIST_LOOP]: [ + constants.PHYSICAL_ACTIVITY, + constants.PREPRANDIAL, + ], + [constants.DIY_LOOP]: [ + constants.PREPRANDIAL, + ], + default: [ + constants.SLEEP, + constants.PHYSICAL_ACTIVITY, + constants.PREPRANDIAL, + ], + }); + }); + }); + describe('AUTOMATED_BASAL_DEVICE_MODELS', () => { it('should define automated basal models per device manufacturer', () => { expect(constants.AUTOMATED_BASAL_DEVICE_MODELS).to.eql({ diff --git a/test/utils/datetime.test.js b/test/utils/datetime.test.js index cfbaf5ec9..5cda3ff13 100644 --- a/test/utils/datetime.test.js +++ b/test/utils/datetime.test.js @@ -544,31 +544,55 @@ describe('datetime', () => { const twoDaysAgo = moment(today).subtract(2, 'days').toISOString(); const thirtyDaysAgo = moment(today).subtract(30, 'days').toISOString(); const thirtyOneDaysAgo = moment(today).subtract(31, 'days').toISOString(); + const oneHourAgo = moment(today).subtract(1, 'hour').toISOString(); + const thirtyMinutesAgo = moment(today).subtract(30, 'minutes').toISOString(); + const thirtySecondsAgo = moment(today).subtract(30, 'seconds').toISOString(); sinon.assert.match(datetime.formatTimeAgo(today, timePrefs), sinon.match({ - text: 'Today', + daysText: 'today', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), })); sinon.assert.match(datetime.formatTimeAgo(yesterday, timePrefs), sinon.match({ - text: 'Yesterday', + daysText: 'yesterday', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 1), })); sinon.assert.match(datetime.formatTimeAgo(twoDaysAgo, timePrefs), sinon.match({ - text: '2 days ago', + daysText: '2 days ago', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 2), })); sinon.assert.match(datetime.formatTimeAgo(thirtyDaysAgo, timePrefs), sinon.match({ - text: '30 days ago', + daysText: '30 days ago', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 30), })); sinon.assert.match(datetime.formatTimeAgo(thirtyOneDaysAgo, timePrefs), sinon.match({ - text: sinon.match(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/), // match YYYY-MM-DD format + daysText: sinon.match(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/), // match YYYY-MM-DD format daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 31), })); + + sinon.assert.match(datetime.formatTimeAgo(oneHourAgo, timePrefs), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + hoursText: '1 hour ago', + hoursAgo: 1, + })); + + sinon.assert.match(datetime.formatTimeAgo(thirtyMinutesAgo, timePrefs), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + minutesText: '30 minutes ago', + minutesAgo: 30, + })); + + sinon.assert.match(datetime.formatTimeAgo(thirtySecondsAgo, timePrefs), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + minutesText: 'a few seconds ago', + minutesAgo: 0, + })); }); it('should return appropriately formatted text strings beyond 30 days when a custom format is supplied', () => { @@ -578,31 +602,55 @@ describe('datetime', () => { const twoDaysAgo = moment(today).subtract(2, 'days').toISOString(); const thirtyDaysAgo = moment(today).subtract(30, 'days').toISOString(); const thirtyOneDaysAgo = moment(today).subtract(31, 'days').toISOString(); + const oneHourAgo = moment(today).subtract(1, 'hour').toISOString(); + const thirtyMinutesAgo = moment(today).subtract(30, 'minutes').toISOString(); + const thirtySecondsAgo = moment(today).subtract(30, 'seconds').toISOString(); sinon.assert.match(datetime.formatTimeAgo(today, timePrefs, customFormat), sinon.match({ - text: 'Today', + daysText: 'today', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), })); sinon.assert.match(datetime.formatTimeAgo(yesterday, timePrefs, customFormat), sinon.match({ - text: 'Yesterday', + daysText: 'yesterday', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 1), })); sinon.assert.match(datetime.formatTimeAgo(twoDaysAgo, timePrefs, customFormat), sinon.match({ - text: '2 days ago', + daysText: '2 days ago', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 2), })); sinon.assert.match(datetime.formatTimeAgo(thirtyDaysAgo, timePrefs, customFormat), sinon.match({ - text: '30 days ago', + daysText: '30 days ago', daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 30), })); sinon.assert.match(datetime.formatTimeAgo(thirtyOneDaysAgo, timePrefs, customFormat), sinon.match({ - text: sinon.match(/^\d{4}-(0[1-9]|1[0-2])$/), // match YYYY-MM format + daysText: sinon.match(/^\d{4}-(0[1-9]|1[0-2])$/), // match YYYY-MM format daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 31), })); + + sinon.assert.match(datetime.formatTimeAgo(oneHourAgo, timePrefs, customFormat), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + hoursText: '1 hour ago', + hoursAgo: 1, + })); + + sinon.assert.match(datetime.formatTimeAgo(thirtyMinutesAgo, timePrefs, customFormat), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + minutesText: '30 minutes ago', + minutesAgo: 30, + })); + + sinon.assert.match(datetime.formatTimeAgo(thirtySecondsAgo, timePrefs, customFormat), sinon.match({ + daysText: 'today', + daysAgo: sinon.match(daysAgo => Math.floor(daysAgo) === 0), + minutesText: 'a few seconds ago', + minutesAgo: 0, + })); }); }); }); diff --git a/test/utils/device.test.js b/test/utils/device.test.js index 43eb4787b..0456d9aed 100644 --- a/test/utils/device.test.js +++ b/test/utils/device.test.js @@ -31,6 +31,7 @@ import { SLEEP, PHYSICAL_ACTIVITY, PREPRANDIAL, + TWIIST_LOOP } from '../../src/utils/constants'; import { types as Types } from '../../data/types'; @@ -144,6 +145,43 @@ describe('device utility functions', () => { }); }); + describe('isTwiistLoop', () => { + it('should return `true` for an upload matching pattern within `client.name` and a major version above 2', () => { + const datum = { type: 'upload', client: { name: 'com.sequelmedtech.tidepool-service', version: '2.0.0' } }; + const datum2 = { type: 'upload', client: { name: 'com.sequelmedtech.tidepool-service', version: '4.10.30' } }; + expect(device.isTwiistLoop(datum)).to.be.true; + expect(device.isTwiistLoop(datum2)).to.be.true; + }); + + it('should return `false` for an upload with non-matching pattern within `client.name`', () => { + const datum = { type: 'upload', client: { name: 'com.tidepool.Loop', version: '2.0.0' } }; + expect(device.isTwiistLoop(datum)).to.be.false; + }); + + it('should return `false` for an upload with client major version below 2', () => { + const datum = { type: 'upload', client: { name: 'com.sequelmedtech.tidepool-service', version: '1.9.9' } }; + expect(device.isTwiistLoop(datum)).to.be.false; + }); + + it('should return `true` for a non-upload data types with matching `origin.name` for Twiist Loop', () => { + const foodDatum = { type: 'food', origin: { name: 'com.dekaresearch.twiist' } }; + const bolusDatum = { type: 'bolus', origin: { name: 'com.dekaresearch.twiist' } }; + const arbitraryDatum = { origin: { name: 'com.dekaresearch.twiist' } }; + expect(device.isTwiistLoop(foodDatum)).to.be.true; + expect(device.isTwiistLoop(bolusDatum)).to.be.true; + expect(device.isTwiistLoop(arbitraryDatum)).to.be.true; + }); + + it('should return `false` for a non-upload data types without matching `origin.name` for Twiist Loop', () => { + const wrongFoodDatum = { type: 'food', origin: { name: 'com.dekaresearch.twiis' } }; + const wrongBolusDatum = { type: 'bolus', origin: { name: 'com.dekaesearch.twist' } }; + const wrongArbitraryDatum = { origin: { name: 'co.dekaresearch.twiist' }, client: { name: 'com.sequelmedtech.tidepool-service', version: '1.9.9' } }; + expect(device.isTwiistLoop(wrongFoodDatum)).to.be.false; + expect(device.isTwiistLoop(wrongBolusDatum)).to.be.false; + expect(device.isTwiistLoop(wrongArbitraryDatum)).to.be.false; + }); + }); + describe('isLoop', () => { it('should return `true` for a matching pattern within `origin.name` for DIY Loop or Tidepool Loop', () => { const diyLoop = { origin: { name: 'com.loopkit.Loop' } }; @@ -166,20 +204,47 @@ describe('device utility functions', () => { expect(device.isLoop(tidepoolLoop)).to.be.true; }); + it('should return `true` for a datum tagged as "loop"', () => { + const loopTaggedDatum = { tags: { loop: true } }; + expect(device.isLoop(loopTaggedDatum)).to.be.true; + }); + it('should return `false` for a non-matching pattern within `client.name`', () => { const diyLoopBad = { client: { name: 'org.loopkit.Loop' } }; const tidepoolLoopBad = { client: { name: 'com.tidepool.Loop' } }; expect(device.isLoop(diyLoopBad)).to.be.false; expect(device.isLoop(tidepoolLoopBad)).to.be.false; }); + + it('should return `true` for an upload datum matching pattern within `client.name` for Twiist Loop and version above 2', () => { + const twiistLoop = { client: { name: 'com.sequelmedtech.tidepool-service', version: '2.0.0' }, type: 'upload' }; + expect(device.isLoop(twiistLoop)).to.be.true; + }); + + it('should return `false` for a upload datum matching pattern within `client.name` for Twiist Loop and version below 2', () => { + const twiistLoop = { client: { name: 'com.sequelmedtech.tidepool-service', version: '1.0.0' }, type: 'upload' }; + expect(device.isLoop(twiistLoop)).to.be.false; + }); + + it('should return `true` for non-upload datum matching pattern within `origin.name` for Twiist Loop', () => { + const twiistLoop = { origin: { name: 'com.dekaresearch.twiist' } }; + expect(device.isLoop(twiistLoop)).to.be.true; + }); + + it('should return `false` for non-upload datum not matching pattern within `origin.name` for Twiist Loop', () => { + const twiistLoop = { origin: { name: 'com.dekaresearch.twist' } }; + expect(device.isLoop(twiistLoop)).to.be.false; + }); }); + describe('isAutomatedBasalDevice', () => { it('should return `true` for an upload record for a pump with automated basal delivery capabilities', () => { expect(device.isAutomatedBasalDevice(MEDTRONIC, {}, '1780')).to.be.true; expect(device.isAutomatedBasalDevice('tandem', { deviceId: 'tandemCIQ123456' })).to.be.true; expect(device.isAutomatedBasalDevice('tidepool loop', { origin: { name: 'org.tidepool.Loop' } })).to.be.true; expect(device.isAutomatedBasalDevice('diy loop', { origin: { name: 'com.loopkit.Loop' } })).to.be.true; + expect(device.isAutomatedBasalDevice('twiist', { origin: { name: 'com.dekaresearch.twiist' } })).to.be.true; }); it('should return `false` for an upload record for a pump without automated basal delivery capabilities', () => { @@ -196,6 +261,7 @@ describe('device utility functions', () => { it('should return `false` for an upload record for a pump without automated bolus delivery capabilities', () => { expect(device.isAutomatedBolusDevice('tandem', { deviceId: 'tandem123456' })).to.be.false; expect(device.isAutomatedBolusDevice('tidepool loop', { origin: { name: 'org.tidepool.Loop' } })).to.be.false; + expect(device.isAutomatedBolusDevice('twiist', { origin: { name: 'com.dekaresearch.twist' } })).to.be.false; }); }); @@ -204,6 +270,7 @@ describe('device utility functions', () => { expect(device.isSettingsOverrideDevice('tandem', { deviceId: 'tandemCIQ123456' })).to.be.true; expect(device.isSettingsOverrideDevice('tidepool loop', { origin: { name: 'org.tidepool.Loop' } })).to.be.true; expect(device.isSettingsOverrideDevice('diy loop', { origin: { name: 'com.loopkit.Loop' } })).to.be.true; + expect(device.isSettingsOverrideDevice('twiist', { origin: { name: 'com.dekaresearch.twiist' } })).to.be.true; }); it('should return `false` for an upload record for a pump without settings override capabilities', () => { @@ -215,6 +282,7 @@ describe('device utility functions', () => { it('should return a pump settings overrides list by manufacturer, with default fallback for manufacturer', () => { expect(device.getSettingsOverrides(TANDEM)).to.have.members([SLEEP, PHYSICAL_ACTIVITY]); expect(device.getSettingsOverrides(TIDEPOOL_LOOP)).to.have.members([PREPRANDIAL, PHYSICAL_ACTIVITY]); + expect(device.getSettingsOverrides(TWIIST_LOOP)).to.have.members([PREPRANDIAL, PHYSICAL_ACTIVITY]); expect(device.getSettingsOverrides(DIY_LOOP)).to.have.members([PREPRANDIAL]); expect(device.getSettingsOverrides(undefined)).to.have.members([SLEEP, PREPRANDIAL, PHYSICAL_ACTIVITY]); }); @@ -273,4 +341,3 @@ describe('device utility functions', () => { }); }); }); -/* eslint-enable max-len */ diff --git a/test/utils/stat.test.js b/test/utils/stat.test.js index ffc0b3500..ee4844139 100644 --- a/test/utils/stat.test.js +++ b/test/utils/stat.test.js @@ -872,7 +872,7 @@ describe('stat', () => { it('should return annotations for `averageDailyDose` stat when viewing multiple days of data', () => { expect(stat.getStatAnnotations(data, commonStats.averageDailyDose, multiDayOpts)).to.have.ordered.members([ - '**Avg. Daily Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view.', + '**Avg. Daily Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view for which we have insulin data.', ]); }); }); @@ -887,7 +887,7 @@ describe('stat', () => { it('should return annotations for `carbs` stat when viewing multiple days of data', () => { expect(stat.getStatAnnotations(data, commonStats.carbs, multiDayOpts)).to.have.ordered.members([ - '**Avg. Daily Carbs**: All carb entries added together, then divided by the number of days in this view. Note, these entries come from either bolus wizard events, or Apple Health records.', + '**Avg. Daily Carbs**: All carb entries added together, then divided by the number of days in this view for which we have carb data. Note, these entries come from either bolus wizard events, or Apple Health records.', 'Derived from _**10**_ carb entries.', ]); }); @@ -1033,14 +1033,14 @@ describe('stat', () => { it('should return annotations for `totalInsulin` stat when viewing a single day of data', () => { expect(stat.getStatAnnotations(data, commonStats.totalInsulin, singleDayOpts)).to.have.ordered.members([ '**Total Insulin:** All basal and bolus insulin delivery (in Units) added together', - '**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for this time period.', + '**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for the time period for which we have insulin data.', ]); }); it('should return annotations for `totalInsulin` stat when viewing multiple days of data', () => { expect(stat.getStatAnnotations(data, commonStats.totalInsulin, multiDayOpts)).to.have.ordered.members([ - '**Total Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view', - '**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for this time period.', + '**Total Insulin:** All basal and bolus insulin delivery (in Units) added together, divided by the number of days in this view for which we have insulin data', + '**How we calculate this:**\n\n**(%)** is the respective total of basal or bolus delivery divided by total insulin delivered for the time period for which we have insulin data.', ]); }); }); @@ -1667,7 +1667,7 @@ describe('stat', () => { }); it('should return title for `bgExtents` stat when bgSource is `cbg`', () => { - expect(stat.getStatTitle(commonStats.bgExtents, cbgOpts)).to.equal('BG Extents (CGM)'); + expect(stat.getStatTitle(commonStats.bgExtents, cbgOpts)).to.equal('Glucose Extents (CGM)'); }); }); diff --git a/yarn.lock b/yarn.lock index 5c396347c..595d020c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4274,8 +4274,8 @@ __metadata: translate-svg-path: 0.0.1 url-loader: 4.1.1 util: 0.12.5 - victory: 31.3.0 - victory-core: 31.2.0 + victory: 37.3.5 + victory-core: 37.3.5 voilab-pdf-table: 0.5.1 webpack: 5.94.0 webpack-cli: 5.1.4 @@ -4283,7 +4283,7 @@ __metadata: babel-core: 6.x || ^7.0.0-bridge.0 classnames: 2.x react: 16.x - react-addons-update: 16.x + react-addons-update: 15.6.x react-dom: 16.x react-redux: 8.x redux: 4.x @@ -4389,6 +4389,75 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:^3.0.3": + version: 3.2.1 + resolution: "@types/d3-array@npm:3.2.1" + checksum: 8a41cee0969e53bab3f56cc15c4e6c9d76868d6daecb2b7d8c9ce71e0ececccc5a8239697cc52dadf5c665f287426de5c8ef31a49e7ad0f36e8846889a383df4 + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 8a0e79a709929502ec4effcee2c786465b9aec51b653ba0b5d05dbfec3e84f418270dd603002d94021885061ff592f614979193bd7a02ad76317f5608560e357 + languageName: node + linkType: hard + +"@types/d3-ease@npm:^3.0.0": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: 0885219966294bfc99548f37297e1c75e75da812a5f3ec941977ebb57dcab0a25acec5b2bbd82d09a49d387daafca08521ca269b7e4c27ddca7768189e987b54 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:^3.0.1": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "*" + checksum: efd2770e174e84fc7316fdafe03cf3688451f767dde1fa6211610137f495be7f3923db7e1723a6961a0e0e9ae0ed969f4f47c038189fa0beb1d556b447922622 + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.1.0 + resolution: "@types/d3-path@npm:3.1.0" + checksum: 1e81b56ed33ba1ac954a8c42c78c3fcf2716927fe5d01b2003591193ad3b639572a3dfcedd9bf78b6b73215a5cfb01cede8f25c936e95ac18fbe3858f9b62f5c + languageName: node + linkType: hard + +"@types/d3-scale@npm:^4.0.2": + version: 4.0.8 + resolution: "@types/d3-scale@npm:4.0.8" + dependencies: + "@types/d3-time": "*" + checksum: 3b1906da895564f73bb3d0415033d9a8aefe7c4f516f970176d5b2ff7a417bd27ae98486e9a9aa0472001dc9885a9204279a1973a985553bdb3ee9bbc1b94018 + languageName: node + linkType: hard + +"@types/d3-shape@npm:^3.1.0": + version: 3.1.7 + resolution: "@types/d3-shape@npm:3.1.7" + dependencies: + "@types/d3-path": "*" + checksum: 776b982e2c4fc04763782af5100993c02bca338632ff2c76d2423ace398300ba7c48cd745f95b5f51edefabbfd026c45829a146c411f8facde09ef92580b20ce + languageName: node + linkType: hard + +"@types/d3-time@npm:*, @types/d3-time@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/d3-time@npm:3.0.4" + checksum: 0c296884571ce70c4bbd4ea9cd1c93c0c8aee602c6c806b056187dd4ee49daf70c2f41da94b25ba0d796edf8ca83cbb87fe6d1cdda7ca669ab800170ece1c12b + languageName: node + linkType: hard + +"@types/d3-timer@npm:^3.0.0": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 1643eebfa5f4ae3eb00b556bbc509444d88078208ec2589ddd8e4a24f230dd4cf2301e9365947e70b1bee33f63aaefab84cd907822aae812b9bc4871b98ab0e1 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0": version: 4.1.9 resolution: "@types/debug@npm:4.1.9" @@ -7185,7 +7254,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:3.2.4": +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:3.2.4, d3-array@npm:^3.1.6": version: 3.2.4 resolution: "d3-array@npm:3.2.4" dependencies: @@ -7194,27 +7263,6 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:^1.2.0": - version: 1.2.4 - resolution: "d3-array@npm:1.2.4" - checksum: d0be1fa7d72dbfac8a3bcffbb669d42bcb9128d8818d84d2b1df0c60bbe4c8e54a798be0457c55a219b399e2c2fabcbd581cbb130eb638b5436b0618d7e56000 - languageName: node - linkType: hard - -"d3-collection@npm:1": - version: 1.0.7 - resolution: "d3-collection@npm:1.0.7" - checksum: 9c6b910a9da0efb021e294509f98263ca4f62d10b997bb30ccfb6edd582b703da36e176b968b5bac815fbb0f328e49643c38cf93b5edf8572a179ba55cf4a09d - languageName: node - linkType: hard - -"d3-color@npm:1": - version: 1.4.1 - resolution: "d3-color@npm:1.4.1" - checksum: a214b61458b5fcb7ad1a84faed0e02918037bab6be37f2d437bf0e2915cbd854d89fbf93754f17b0781c89e39d46704633d05a2bfae77e6209f0f4b140f9894b - languageName: node - linkType: hard - "d3-color@npm:1 - 3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" @@ -7222,17 +7270,10 @@ __metadata: languageName: node linkType: hard -"d3-ease@npm:^1.0.0": - version: 1.0.7 - resolution: "d3-ease@npm:1.0.7" - checksum: 117811d51dfc4a126e8d23d249252df792fbbe30a93615e1d67158c482eff69b900e45a4cc92746fe65b1143287455406a89aae04eb4ca1ba5b1dc2a42af5b85 - languageName: node - linkType: hard - -"d3-format@npm:1": - version: 1.4.5 - resolution: "d3-format@npm:1.4.5" - checksum: 1b8b2c0bca182173bccd290a43e8b635a83fc8cfe52ec878c7bdabb997d47daac11f2b175cebbe73f807f782ad655f542bdfe18180ca5eb3498a3a82da1e06ab +"d3-ease@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd languageName: node linkType: hard @@ -7243,16 +7284,7 @@ __metadata: languageName: node linkType: hard -"d3-interpolate@npm:1, d3-interpolate@npm:^1.1.1": - version: 1.4.0 - resolution: "d3-interpolate@npm:1.4.0" - dependencies: - d3-color: 1 - checksum: d98988bd1e2f59d01f100d0a19315ad8f82ef022aa09a65aff76f747a44f9b52f2d64c6578b8f47e01f2b14a8f0ef88f5460d11173c0dd2d58238c217ac0ec03 - languageName: node - linkType: hard - -"d3-interpolate@npm:1.2.0 - 3": +"d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" dependencies: @@ -7261,13 +7293,6 @@ __metadata: languageName: node linkType: hard -"d3-path@npm:1": - version: 1.0.9 - resolution: "d3-path@npm:1.0.9" - checksum: d4382573baf9509a143f40944baeff9fead136926aed6872f7ead5b3555d68925f8a37935841dd51f1d70b65a294fe35c065b0906fb6e42109295f6598fc16d0 - languageName: node - linkType: hard - "d3-path@npm:^3.1.0": version: 3.1.0 resolution: "d3-path@npm:3.1.0" @@ -7275,7 +7300,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:4.0.2": +"d3-scale@npm:4.0.2, d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -7288,22 +7313,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:^1.0.0": - version: 1.0.7 - resolution: "d3-scale@npm:1.0.7" - dependencies: - d3-array: ^1.2.0 - d3-collection: 1 - d3-color: 1 - d3-format: 1 - d3-interpolate: 1 - d3-time: 1 - d3-time-format: 2 - checksum: c889c510aa0380b23e3a595be6b143b7053351ab6fe212918aa1ae80353a9ccde81064c2afa95dbdcd936fbb0c69dd560de291cbbea80d068a4390a3f67596aa - languageName: node - linkType: hard - -"d3-shape@npm:3.2.0": +"d3-shape@npm:3.2.0, d3-shape@npm:^3.1.0": version: 3.2.0 resolution: "d3-shape@npm:3.2.0" dependencies: @@ -7312,24 +7322,6 @@ __metadata: languageName: node linkType: hard -"d3-shape@npm:^1.0.0, d3-shape@npm:^1.2.0": - version: 1.3.7 - resolution: "d3-shape@npm:1.3.7" - dependencies: - d3-path: 1 - checksum: 46566a3ab64a25023653bf59d64e81e9e6c987e95be985d81c5cedabae5838bd55f4a201a6b69069ca862eb63594cd263cac9034afc2b0e5664dfe286c866129 - languageName: node - linkType: hard - -"d3-time-format@npm:2": - version: 2.3.0 - resolution: "d3-time-format@npm:2.3.0" - dependencies: - d3-time: 1 - checksum: 5445eaaf2b3b2095cdc1fa75dfd2f361a61c39b677dcc1c2ba4cb6bc0442953de0fbaaa397d7d7a9325ad99c63d869f162a713e150e826ff8af482615664cb3f - languageName: node - linkType: hard - "d3-time-format@npm:2 - 4, d3-time-format@npm:4.1.0": version: 4.1.0 resolution: "d3-time-format@npm:4.1.0" @@ -7339,14 +7331,7 @@ __metadata: languageName: node linkType: hard -"d3-time@npm:1": - version: 1.1.0 - resolution: "d3-time@npm:1.1.0" - checksum: 33fcfff94ff093dde2048c190ecca8b39fe0ec8b3c61e9fc39c5f6072ce5b86dd2b91823f086366995422bbbac7f74fd9abdb7efe4f292a73b1c6197c699cc78 - languageName: node - linkType: hard - -"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3.1.0": +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:3.1.0, d3-time@npm:^3.0.0": version: 3.1.0 resolution: "d3-time@npm:3.1.0" dependencies: @@ -7355,14 +7340,14 @@ __metadata: languageName: node linkType: hard -"d3-timer@npm:^1.0.0": - version: 1.0.10 - resolution: "d3-timer@npm:1.0.10" - checksum: f7040953672deb2dfa03830ace80dbbcb212f80890218eba15dcca6f33f74102d943023ccc2a563295195cd8c63639bb2410ef1691c8fecff4a114fdf5c666f4 +"d3-timer@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73 languageName: node linkType: hard -"d3-voronoi@npm:^1.1.2": +"d3-voronoi@npm:^1.1.4": version: 1.1.4 resolution: "d3-voronoi@npm:1.1.4" checksum: d28a74bc62f2b936b0d3b51d5be8d2366afca4fd7026d7ee8f655600650bf0c985da38a8c3ae46bfa315b5f524f3ca1c5211437cf1c8c737cc1da681e015baee @@ -7603,6 +7588,22 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:^4.0.0": + version: 4.0.1 + resolution: "delaunator@npm:4.0.1" + checksum: a49f1c23edbcb79079a13577d32fcd46d0db30879c8484f742a0d840923085f2f3de35a9bfbb96eadd12201ffb7c3adf45b0f528d08b71cb547c5f8068b5d61b + languageName: node + linkType: hard + +"delaunay-find@npm:0.0.6": + version: 0.0.6 + resolution: "delaunay-find@npm:0.0.6" + dependencies: + delaunator: ^4.0.0 + checksum: 072e197a4317dd06ff8349dfa6731f62d322c7ba4697d4a323da7798676f5c429c4ac691ae5207f7c7da567eca7c71dada896206cbd7995e6e9d145101734c31 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -11214,6 +11215,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee + languageName: node + linkType: hard + "json5@npm:^1.0.1, json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" @@ -14059,10 +14067,10 @@ __metadata: languageName: node linkType: hard -"react-fast-compare@npm:^2.0.0": - version: 2.0.4 - resolution: "react-fast-compare@npm:2.0.4" - checksum: 06046595f90a4e3e3a56f40a8078c00aa71bdb064ddb98343f577f546aa22e888831fd45f009c93b34707cc842b4c637737e956fd13d6f80607ee92fb9cf9a1c +"react-fast-compare@npm:^3.2.0": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 languageName: node linkType: hard @@ -16837,335 +16845,417 @@ __metadata: languageName: node linkType: hard -"victory-area@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-area@npm:31.2.0" +"victory-area@npm:37.3.5": + version: 37.3.5 + resolution: "victory-area@npm:37.3.5" dependencies: - d3-shape: ^1.2.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 56267af197297232c04f832463a28fe148d6bda5f29d82a49fbecd49f2cd35b486d9288a445ba67a3ab4c265f2d77c267dcdf96ac68f1fffbc7ea446b10166ae + lodash: ^4.17.19 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: c6769023a4d657d8cf03e7eaddf7edc5ddb70269e74c8a13f6bd247a42eb4677b205c5094ddd14dfd33694d401744efa8998ded38ce930e25d1d44f4d34f0957 languageName: node linkType: hard -"victory-axis@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-axis@npm:31.2.0" +"victory-axis@npm:37.3.5": + version: 37.3.5 + resolution: "victory-axis@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: dea086b8ec43be8790f6806a0ff522892daa2b1d430bb338befd7f7c04a11307e95b77c18f89433f428b914cd745fce58e31d7f25f147cfc14e8401497447748 + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 2011097a9241ce43dce8cfdf1018977b4a958093a661edcf88849215336b792a9ccf9bb9851605a20f2752c87c5b5fb82cb9c86dcdf5943450faa73a7f5f9052 languageName: node linkType: hard -"victory-bar@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-bar@npm:31.2.0" +"victory-bar@npm:37.3.5": + version: 37.3.5 + resolution: "victory-bar@npm:37.3.5" dependencies: - d3-shape: ^1.2.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: b622759f818c682777caa2d7d7c1ed99cabc34280c91656d12268fdac67a0a5cbf2ed9b0b5a79a7ea4dd919ad231da44987eecd7f9e8ce61ffc7865c91fd9299 + lodash: ^4.17.19 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: be1c95b8c1320eac20bff9504f579e2eb5e7f1fed7d8363246194535b8dc7e0316a2b62cdeac4bb43898adc93345a28ee946014ef7015206b4f85543ef2ee365 languageName: node linkType: hard -"victory-box-plot@npm:^31.3.0": - version: 31.3.0 - resolution: "victory-box-plot@npm:31.3.0" +"victory-box-plot@npm:37.3.5": + version: 37.3.5 + resolution: "victory-box-plot@npm:37.3.5" dependencies: - d3-array: ^1.2.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: a5b18b7c378236cf30e3a21786c02e866885613a603040f79d06403eed0b0631bbcb60e004a3b0cc8afea01c76dd127b106f524c6feb85c839f918855026248b + lodash: ^4.17.19 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 93c8bf2fd523be7f1c30d67fe7574ead43f2e63f914094f27ebd61bebf219dbd2e0f8f4e8382d6eee2aec4bb0261463e8801384145e86c5fdadc2c37fb22c280 languageName: node linkType: hard -"victory-brush-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-brush-container@npm:31.2.0" +"victory-brush-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-brush-container@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: b4749b5ee388c1c5d247234276198cae3c23eff13315181c0b8d7737ffffbe757187d4550bf7e8bd8c9321d67d26e685b859189cfb241fda15b1185438f40ba7 + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 73f989ecd527440c5409282256b8ed2f7425baf17727e3d98286915fc424106cf62320167d3d11c03bba80154a6611235eb1a9ece3b568c92897b1378be158f5 languageName: node linkType: hard -"victory-brush-line@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-brush-line@npm:31.2.0" +"victory-brush-line@npm:37.3.5": + version: 37.3.5 + resolution: "victory-brush-line@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: a6a060b17102b8149a94f83e94c72f06eb877bba7c5e5154190c85890ed01565b7087604c5f580d7ac5479fee6ecb9e41045f3f2749453debdd9a68de82aff5c + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: d544fc2c668b048b7d1896fc699e876b2df29882df7272bda917f149a3e2c2947686d06c5c871c67a8f999dd438eab91432aa62fb84e97fab6be05293f5bf7e9 languageName: node linkType: hard -"victory-candlestick@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-candlestick@npm:31.2.0" +"victory-candlestick@npm:37.3.5": + version: 37.3.5 + resolution: "victory-candlestick@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 95cf304d8ae8c50087cd094c1e18dd73a453da91b75333cddf427f7906672e1163cd195e10b1b3373d1f2a4f9084d8046928d1c544e4ce1c2c1a9ca31269cf21 + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 60b91488cb29b6d68fd724c3248274f0c6590c16e833bb85b1fbaa64e0f56791e081fce963ff90ec054da64348212d74b6f5ea337f4e1667b5494a2f6d173e90 languageName: node linkType: hard -"victory-chart@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-chart@npm:31.2.0" +"victory-canvas@npm:37.3.5": + version: 37.3.5 + resolution: "victory-canvas@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - react-fast-compare: ^2.0.0 - victory-axis: ^31.2.0 - victory-core: ^31.2.0 - victory-polar-axis: ^31.2.0 - victory-shared-events: ^31.2.0 - checksum: 88a8861f2fae6edf38b65285990bc6a8e8026303b9e302e70a233d90d1f684d08e5bd3f06eb5ac01300ce3b1f37c8aa2f8bad2d8797e9eee75da03169eddf8d2 + lodash: ^4.17.19 + victory-bar: 37.3.5 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 4358554efddf4eb23326aaedb68b393925d2483cfe2dbad7163c28185de3fef4cf12011b6fdad1bddccc8dbe71c2450dc1af349dfc73e48cfb8103de9f854ea6 languageName: node linkType: hard -"victory-core@npm:31.2.0, victory-core@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-core@npm:31.2.0" +"victory-chart@npm:37.3.5": + version: 37.3.5 + resolution: "victory-chart@npm:37.3.5" dependencies: - d3-ease: ^1.0.0 - d3-interpolate: ^1.1.1 - d3-scale: ^1.0.0 - d3-shape: ^1.2.0 - d3-timer: ^1.0.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - react-fast-compare: ^2.0.0 - checksum: 0b590c282a58c20f20218e97f7031cfd427c8aa6dbe08dfe169127a13d1873ef5c715015ff69c25e18e97eeaf142c2dfc3d5f89aa9eb501887c20173223ff159 + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-axis: 37.3.5 + victory-core: 37.3.5 + victory-polar-axis: 37.3.5 + victory-shared-events: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 44d42fafac9a108b0641817e937834d88085ebc4857236ff4c92bd02d858c4243e1e6525a34ec41718d993583bb869182ef3153278a20e71d9986691b7674b54 languageName: node linkType: hard -"victory-create-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-create-container@npm:31.2.0" +"victory-core@npm:37.3.5": + version: 37.3.5 + resolution: "victory-core@npm:37.3.5" dependencies: - lodash: ^4.17.5 - victory-brush-container: ^31.2.0 - victory-core: ^31.2.0 - victory-cursor-container: ^31.2.0 - victory-selection-container: ^31.2.0 - victory-voronoi-container: ^31.2.0 - victory-zoom-container: ^31.2.0 - checksum: 014ecaac7415c0dace1664127b203e994720de58521f78f08e7b7f080ad8c97fdc3aa16b9ee97c4ee755bb55a411e42109d9f3d34ff164347ecd9c7d4e6eb855 + lodash: ^4.17.21 + react-fast-compare: ^3.2.0 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: cb021419e6cfee495868b7a2e419235b6858f2d17b0a1ab2a7b79429e759e836985a04a9ca8446aa928e76d51f185cedd4eae82204168e4b3420084a429f1533 languageName: node linkType: hard -"victory-cursor-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-cursor-container@npm:31.2.0" +"victory-create-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-create-container@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 9c400c95702f4ce27992b943841a228a974a17b69124e3e482d45c7e6ad0f7f17ae5d528db48e260f0b1c96cba652d469fa306b4b7ea53e67bfbe046d79d347b + lodash: ^4.17.19 + victory-brush-container: 37.3.5 + victory-core: 37.3.5 + victory-cursor-container: 37.3.5 + victory-selection-container: 37.3.5 + victory-voronoi-container: 37.3.5 + victory-zoom-container: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: f7b946245f0cc2cb7017521902a892d603006e00ca73190c7a1ccd335c3367db9c4af4bd3c28e3b03a345800b39a5dc8ef00efc7032f92827df09aa0b6d962e6 languageName: node linkType: hard -"victory-errorbar@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-errorbar@npm:31.2.0" +"victory-cursor-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-cursor-container@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 2cee41517b24cd92141d6afdac7fbd2ba465f6c5ef92ecf71a36a542808ed068c42800b49f3293cb28c6161ce1eccc507f5bab7c11d88a7b93715dfd16ae3f3b + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: f5b2224e9a920909287f79ac7978f44bd6eabb3394afc79116cd7e452368575e1e7e6907ce7f0a5626aa826b5bdebe4c7684402eab69fb9b93b1da42f76e1ce2 languageName: node linkType: hard -"victory-group@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-group@npm:31.2.0" +"victory-errorbar@npm:37.3.5": + version: 37.3.5 + resolution: "victory-errorbar@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - react-fast-compare: ^2.0.0 - victory-core: ^31.2.0 - checksum: d457c7309e1d59897c3647b9efadfa29015ec8d57d5c4813093efa925c28c06091b68dfca6d9fc6f432663cb98c7614626123ef0c2617bbfed65ddc7948edf20 + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: c3008f16ef98e0447545e1a060a9c6a7849693c815e0b2e148877e79937d94e2554204e4f8ae3eb30cff0570232df5e181313fa925fb81b4f963cb83d128fe87 languageName: node linkType: hard -"victory-legend@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-legend@npm:31.2.0" +"victory-group@npm:37.3.5": + version: 37.3.5 + resolution: "victory-group@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 20edeeeb4ff421e5fed166dbd55207da7ad2dfa644ec12b98c8612d3e428267fee2df9b92bf6581ca57a38a7e16a1aeb15fe9819794a5e9bc11fed26562c14df + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + victory-shared-events: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 0b661a9d4fa8dfd4674f9461883eedc4b4755659803e5d249b63df13a5c9ac1f75e00a23d4d8f80a1c34f30ac467aa8a0b509bfb978d9211814ca7fc0c53e304 languageName: node linkType: hard -"victory-line@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-line@npm:31.2.0" +"victory-histogram@npm:37.3.5": + version: 37.3.5 + resolution: "victory-histogram@npm:37.3.5" dependencies: - d3-shape: ^1.2.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: b460f98167553c131f2ecf31caac8744b09bce9762e75ba87caf4d9e29cdfba8c5a4ec1ffc283ada0cab344bcfd33c9a85d1d7603c3fcde6d4052f2faa940003 + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-bar: 37.3.5 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 62e55975eb9d64fbcbf1af41eb5115bfc6d79e788e8171445cd38006147320576d385e01c643e2c0cb4a2ae087c7fa2f076e885a2c2dbfda53ff10a4f7996746 languageName: node linkType: hard -"victory-pie@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-pie@npm:31.2.0" +"victory-legend@npm:37.3.5": + version: 37.3.5 + resolution: "victory-legend@npm:37.3.5" dependencies: - d3-shape: ^1.0.0 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 333f2aeb099cb3cb4b3d6da49c1cb3a576be9b2f87faf032475e7689b291c12643b672c683751e2c86931da2b64e7d243c30215f083f562e5ac460744dad2ada + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 53a06bfef56066a0cfffcca54233f40befaebd6cfa96e014e09d6691bb59789225fb19652f43ec4de25acc62cbde8e89d34aa7c7196e838d459134542cb1aa4e languageName: node linkType: hard -"victory-polar-axis@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-polar-axis@npm:31.2.0" +"victory-line@npm:37.3.5": + version: 37.3.5 + resolution: "victory-line@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: b13e62c2b32228054a7acc5dac1a3bfc76b8596fa48bb78378b1b32ce9c44e5dcd928b0fd2685655f7f3b298c086aecde8fd876c5f5844a7ee526f20aafcee77 + lodash: ^4.17.19 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: d602a12e759ee59d542acc82de25c5910833cf03836d12d9ef8ab026a954baddc2df60d331cfbf6b44eb1617da543f6d2f11a8cce43038d94eeb2a8db16d022c languageName: node linkType: hard -"victory-scatter@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-scatter@npm:31.2.0" +"victory-pie@npm:37.3.5": + version: 37.3.5 + resolution: "victory-pie@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: 36801a195b208be4bac40b0bb5664e32322ced09dc4322bc84c76e006881ae39f44d16a9958c74f26f6165b7abb527db88bc6436873c31da1cf078f831dd0bf3 + lodash: ^4.17.19 + victory-core: 37.3.5 + victory-vendor: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: ce4054abf483c654298271e693202945402b9e38dc1fb98827701277a1b14cde5c5f77fc04b497de4603dc133665bf104679e47734d24ce2bf2d45eb84ca4922 languageName: node linkType: hard -"victory-selection-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-selection-container@npm:31.2.0" +"victory-polar-axis@npm:37.3.5": + version: 37.3.5 + resolution: "victory-polar-axis@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: f5214c645b306ec05fefb8cb38b98218854fec85142cabb89ccea5004fc2473a3cd6092d64dce6e22727b3b5567f77de649c16e8a6168ebdd21d91d53d400aaa + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 506f8f1be5762afe16bb0e5fbb329b511e5b676524ffba3a82ecd39e1532864f280e6ebf723824a856fd1c86e633cba5deecf2a6dcf9d20203aca52940f9b303 languageName: node linkType: hard -"victory-shared-events@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-shared-events@npm:31.2.0" +"victory-scatter@npm:37.3.5": + version: 37.3.5 + resolution: "victory-scatter@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - react-fast-compare: ^2.0.0 - victory-core: ^31.2.0 - checksum: aacf20f75c56a6dc9e2c403756b12c398b6300c42a796d9b8816336a6021c7533e044546239f0707432cf558a58cf3c5dd7324e2a4a67a8584111fac2648b3dc + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 3f0908b34646e31232e6d728633b5f0e652830c7f2f3eec7f91354575677ce236e31a317a59bf80f5f80cf8cc2adb3b20595f4a3280d3fd976a18f2d79fb8c55 languageName: node linkType: hard -"victory-stack@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-stack@npm:31.2.0" +"victory-selection-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-selection-container@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - react-fast-compare: ^2.0.0 - victory-core: ^31.2.0 - checksum: 2f5212c9572c13a4b48df9d6e6ea18178b8dbcd934226fa0301794f1ee24d2e06fc3e4495a8e3aa8d924fa0a08c6bc4fd1e8f38ebe84cff8fcfe1b62aa687025 + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: d98f220a74e6c3ff417a0c991eede28d78acca4cf97f86dcce2c9a966b5d084cdd2cd5a67c4dbb5da23d064ee808f3adb8b206401c3012a41f63973b237293b0 languageName: node linkType: hard -"victory-tooltip@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-tooltip@npm:31.2.0" +"victory-shared-events@npm:37.3.5": + version: 37.3.5 + resolution: "victory-shared-events@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: b2090a3fb0726331139d4ac79b74fe9de2062297946584b50c6593e588645109cbf4080b5154fa713d9045fb093ff02b9deeaaf0f4fa39bd1a01613115d95f28 + json-stringify-safe: ^5.0.1 + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 01a61c1a94c979a22d52c90fd834d24728bb23664ac30ee161d62ce34f41f782d90956c5f345d4b3b7e598d903584eb026a244ea19d6208c99956bcd06be8e0f languageName: node linkType: hard -"victory-voronoi-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-voronoi-container@npm:31.2.0" +"victory-stack@npm:37.3.5": + version: 37.3.5 + resolution: "victory-stack@npm:37.3.5" dependencies: - d3-voronoi: ^1.1.2 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - victory-tooltip: ^31.2.0 - checksum: 58fd0f90836abe1788af7168ffa52a3dc0b7f660f5cd0b4ad1b87ad0a7053cc762102c85bfae06e1e5a7bd80acf7df707028e3093a3ed4185f7d906a84f31dea + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + victory-shared-events: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 3eedf72b2c35fdd9ade500075230478a641041f677ec384d8e3fcaafebd4b2bc12bd8c49d10c8c0b94916d8ece1217bbe3808ec38af08679bc486724390127e8 languageName: node linkType: hard -"victory-voronoi@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-voronoi@npm:31.2.0" +"victory-tooltip@npm:37.3.5": + version: 37.3.5 + resolution: "victory-tooltip@npm:37.3.5" dependencies: - d3-voronoi: ^1.1.2 - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: bb95d83b0910422ea9f0135ceeea01996d866bd8308231a443be051b217f7b7397b6d0b37d88b1cffae85f20bdcf82ecfb2bdb15e09f1e9cdf7b19eb53b27c9e + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 76b78b8673972d146dd6ca4b7e2b7612bb505c598d478126bcbd172bb8cceb54809cafdb02598d5ef242b7fc1735fb07dff1159a340501402b57a16f790b22d9 languageName: node linkType: hard -"victory-zoom-container@npm:^31.2.0": - version: 31.2.0 - resolution: "victory-zoom-container@npm:31.2.0" +"victory-vendor@npm:37.3.5": + version: 37.3.5 + resolution: "victory-vendor@npm:37.3.5" dependencies: - lodash: ^4.17.5 - prop-types: ^15.5.8 - victory-core: ^31.2.0 - checksum: e0e1d2d92d09a572fc67f570f74b59e19de856f6379e3ee8464b36acbaa388eacb04cb1c97c4f03387ff0168b8ebada4d9bc26eff62b76224f7f56de485aff22 - languageName: node - linkType: hard - -"victory@npm:31.3.0": - version: 31.3.0 - resolution: "victory@npm:31.3.0" - dependencies: - victory-area: ^31.2.0 - victory-axis: ^31.2.0 - victory-bar: ^31.2.0 - victory-box-plot: ^31.3.0 - victory-brush-container: ^31.2.0 - victory-brush-line: ^31.2.0 - victory-candlestick: ^31.2.0 - victory-chart: ^31.2.0 - victory-core: ^31.2.0 - victory-create-container: ^31.2.0 - victory-cursor-container: ^31.2.0 - victory-errorbar: ^31.2.0 - victory-group: ^31.2.0 - victory-legend: ^31.2.0 - victory-line: ^31.2.0 - victory-pie: ^31.2.0 - victory-polar-axis: ^31.2.0 - victory-scatter: ^31.2.0 - victory-selection-container: ^31.2.0 - victory-shared-events: ^31.2.0 - victory-stack: ^31.2.0 - victory-tooltip: ^31.2.0 - victory-voronoi: ^31.2.0 - victory-voronoi-container: ^31.2.0 - victory-zoom-container: ^31.2.0 - checksum: f73c720aa2dda91418c5d2317b30b92aaf5ae9ebd726fa1fb472ef4ef5d98df2651b7007f600a0d9b3b826eee56a08e436a4e064b446ec6a685fff2108f17c1d + "@types/d3-array": ^3.0.3 + "@types/d3-ease": ^3.0.0 + "@types/d3-interpolate": ^3.0.1 + "@types/d3-scale": ^4.0.2 + "@types/d3-shape": ^3.1.0 + "@types/d3-time": ^3.0.0 + "@types/d3-timer": ^3.0.0 + d3-array: ^3.1.6 + d3-ease: ^3.0.1 + d3-interpolate: ^3.0.1 + d3-scale: ^4.0.2 + d3-shape: ^3.1.0 + d3-time: ^3.0.0 + d3-timer: ^3.0.1 + checksum: 6dfd0dc0e8c1eaade896b472c7e8d9977ca751be88fb478f2a6eec34dd5d6412be4410399550f4096de47c36af968910f8d67892d0c1a5242b057d5b5ca3a5dc + languageName: node + linkType: hard + +"victory-voronoi-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-voronoi-container@npm:37.3.5" + dependencies: + delaunay-find: 0.0.6 + lodash: ^4.17.19 + react-fast-compare: ^3.2.0 + victory-core: 37.3.5 + victory-tooltip: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: e02199145961851c4377296413e0b780ec617c7c1139a7fbe2cce90299487c50e756011a7c4f1347f3fecc7a5df915d5430eff00bc555526dae806dbe4c820cd + languageName: node + linkType: hard + +"victory-voronoi@npm:37.3.5": + version: 37.3.5 + resolution: "victory-voronoi@npm:37.3.5" + dependencies: + d3-voronoi: ^1.1.4 + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 343e6a3ae12960f07fa221e12265e75d1d1cf80a6e0ddb84e1f4bb38ae3f252dd6af4dde74a4f426abb2a226570a12af8a6291caabaacf680782267a537190cc + languageName: node + linkType: hard + +"victory-zoom-container@npm:37.3.5": + version: 37.3.5 + resolution: "victory-zoom-container@npm:37.3.5" + dependencies: + lodash: ^4.17.19 + victory-core: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 9ab47aa2934c3e062c5259f1456d7071c05337db9c54b3d35e25d8ae78a82e28878a7ae73f1071aff5ff3e0b0d733825293fe9e2dfdd99dfa96e7f25fb59c6e8 + languageName: node + linkType: hard + +"victory@npm:37.3.5": + version: 37.3.5 + resolution: "victory@npm:37.3.5" + dependencies: + victory-area: 37.3.5 + victory-axis: 37.3.5 + victory-bar: 37.3.5 + victory-box-plot: 37.3.5 + victory-brush-container: 37.3.5 + victory-brush-line: 37.3.5 + victory-candlestick: 37.3.5 + victory-canvas: 37.3.5 + victory-chart: 37.3.5 + victory-core: 37.3.5 + victory-create-container: 37.3.5 + victory-cursor-container: 37.3.5 + victory-errorbar: 37.3.5 + victory-group: 37.3.5 + victory-histogram: 37.3.5 + victory-legend: 37.3.5 + victory-line: 37.3.5 + victory-pie: 37.3.5 + victory-polar-axis: 37.3.5 + victory-scatter: 37.3.5 + victory-selection-container: 37.3.5 + victory-shared-events: 37.3.5 + victory-stack: 37.3.5 + victory-tooltip: 37.3.5 + victory-voronoi: 37.3.5 + victory-voronoi-container: 37.3.5 + victory-zoom-container: 37.3.5 + peerDependencies: + react: ">=16.6.0" + checksum: 6693417c580810bfc59b70b6fa386d58214b4d3e14c2e10a067a83caf800a3fb93b89671d01e634b6dc84d60dc3a1781836ec6df6786c5ef21ba692ca43c84c3 languageName: node linkType: hard