Skip to content

Commit

Permalink
Merge pull request #511 from lifeomic/sleep-summary
Browse files Browse the repository at this point in the history
Add Sleep Summary View
  • Loading branch information
sternetj committed Jan 11, 2024
2 parents c497ae3 + cbe0ba8 commit f485f36
Show file tree
Hide file tree
Showing 6 changed files with 725 additions and 73 deletions.
72 changes: 11 additions & 61 deletions src/components/MyData/SleepChart/DailyChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import {
import { useCommonChartProps } from '../useCommonChartProps';
import uniqBy from 'lodash/unionBy';
import type { SleepChartData } from './useSleepChartData';
import { useSleepDisplay, valToName } from './useSleepDisplay';
import { DataSelector, SleepDataToolTipPreparer } from './DataSelector';
import ViewShot from 'react-native-view-shot';
import { t } from 'i18next';
import { useVictoryTheme } from '../useVictoryTheme';
import { useStyles } from '../../../hooks';
import { CodeableConcept } from 'fhir/r3';
import orderBy from 'lodash/orderBy';
import { Falsey } from 'lodash';
import { scaleLinear } from 'd3-scale';
Expand All @@ -44,6 +44,7 @@ export const DailyChart = (props: Props) => {
const common = useCommonChartProps();
const { sleepAnalysisTheme: theme } = useVictoryTheme();
const { styles } = useStyles(defaultStyles);
const toDisplay = useSleepDisplay(styles.stageColors);

const yDomain = useMemo(
() =>
Expand All @@ -66,7 +67,9 @@ export const DailyChart = (props: Props) => {
return undefined;
}

const sleepType = codeToNum(d.code);
const display = toDisplay(d.code);

const sleepType = display.num;
const start = startOfMinute(new Date(d.valuePeriod.start));
const end = endOfMinute(new Date(d.valuePeriod.end));

Expand All @@ -77,11 +80,8 @@ export const DailyChart = (props: Props) => {
y: yDomain(4 - sleepType) + common.padding.top,
height: yDomain(sleepType),
sleepType,
fill: codeToValue({
default: 'transparent',
...styles.stageColors,
})(d.code),
sleepTypeName: valToName(sleepType),
fill: display.color,
sleepTypeName: display.name,
startTime: start.toLocaleTimeString(),
durationInMinutes: Math.abs(differenceInMinutes(start, end)),
};
Expand All @@ -90,7 +90,7 @@ export const DailyChart = (props: Props) => {
'sleepType',
'asc',
),
[sleepData, styles, common, xDomain, yDomain],
[sleepData, common, xDomain, yDomain, toDisplay],
);

const ticks = useMemo(() => {
Expand Down Expand Up @@ -187,59 +187,6 @@ function compact<T>(arr: T[]): Exclude<T, Falsey>[] {
return arr.filter((v) => !!v) as Exclude<T, Falsey>[];
}

type CodeMap<T> = Partial<Record<'light' | 'awake' | 'deep' | 'rem', T>> &
Record<'default', T>;

function codeToValue<T>(map: CodeMap<T>) {
return (coding: CodeableConcept) => {
for (const code of coding.coding ?? []) {
if (
code.system === 'http://loinc.org' &&
code.code === '93830-8' &&
map.light
) {
return map.light;
} else if (
code.system === 'http://loinc.org' &&
code.code === '93831-6' &&
map.deep
) {
return map.deep;
} else if (
code.system === 'http://loinc.org' &&
code.code === '93829-0' &&
map.rem
) {
return map.rem;
} else if (
code.system === 'http://loinc.org' &&
code.code === '93828-2' &&
map.awake
) {
return map.awake;
}
}

return map.default;
};
}

const codeToNum = codeToValue({
deep: 1,
light: 2,
rem: 3,
awake: 4,
default: 0,
});

const valToName = (value: number) =>
({
4: t('sleep-analysis-type-awake', 'Awake'),
3: t('sleep-analysis-type-rem', 'REM'),
2: t('sleep-analysis-type-light', 'Light'),
1: t('sleep-analysis-type-deep', 'Deep'),
}[value] ?? '');

type TickProps = {
text?: string;
index?: number;
Expand Down Expand Up @@ -291,6 +238,9 @@ const defaultStyles = createStyles('SleepAnalysisSingleDay', (theme) => ({
fill: theme.colors.primary,
r: 10,
} as CircleProps,
/**
* @deprecated Use SleepAnalysisDisplay to control the stage colors
*/
stageColors: {
awake: 'hotpink',
deep: 'dodgerblue',
Expand Down
63 changes: 62 additions & 1 deletion src/components/MyData/SleepChart/MultiDayChart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { VictoryChart, VictoryAxis, VictoryBar } from 'victory-native';
import { VictoryLabelProps } from 'victory-core';
import { BarProps } from 'victory-bar';
import {
eachDayOfInterval,
format,
Expand All @@ -12,7 +13,7 @@ import {
isValid,
} from 'date-fns';
import groupBy from 'lodash/groupBy';
import { G, Circle, Text, SvgProps } from 'react-native-svg';
import { G, Circle, Text, SvgProps, Rect } from 'react-native-svg';
import { useCommonChartProps } from '../useCommonChartProps';
import type { SleepChartData } from './useSleepChartData';
import { useVictoryTheme } from '../useVictoryTheme';
Expand All @@ -23,6 +24,9 @@ import { useStyles } from '../../../hooks';
import { createStyles } from '../../BrandConfigProvider';
import { View } from 'react-native';
import { ActivityIndicatorView } from '../../ActivityIndicatorView';
import { scaleLinear } from 'd3-scale';
import { useSleepDisplay } from './useSleepDisplay';
import { ObservationComponent } from 'fhir/r3';

type Props = SleepChartData & {
domainPadding?: number;
Expand Down Expand Up @@ -85,6 +89,7 @@ export const MultiDayChart = (props: Props) => {
minutes: Math.round(duration % 60),
isAverage: isYearChart,
period: format(new Date(date), isYearChart ? 'MMMM' : 'MMMM d'),
components: d.flatMap((datum) => datum.component),
};
}),
};
Expand Down Expand Up @@ -174,6 +179,7 @@ export const MultiDayChart = (props: Props) => {
cornerRadius={barWidth / 2}
labels={(label) => label}
labelComponent={<A11yBarLabel />}
dataComponent={<SleepBar />}
style={{
...theme.bar?.style,
data: {
Expand Down Expand Up @@ -209,6 +215,61 @@ export const MultiDayChart = (props: Props) => {
);
};

const SleepBar = (props: BarProps) => {
const toDisplay = useSleepDisplay();
const stages = {
deep: 0,
light: 0,
rem: 0,
awake: 0,
default: 0,
};
let total = 0;

props.datum.components.forEach((component: ObservationComponent) => {
if (!component?.valuePeriod?.start || !component?.valuePeriod?.end) {
return;
}

const cStart = Number(new Date(component.valuePeriod.start));
const cEnd = Number(new Date(component.valuePeriod.end));
const diff = cEnd - cStart;

const display = toDisplay(component.code);

stages[display.codeType] += diff;
total += diff;
});

const scale = scaleLinear()
.range([0, (props.y0 ?? 0) - (props.y ?? 0)])
.domain([0, total]);

let y = props.y ?? 0;

return (
<>
{(['awake', 'rem', 'light', 'deep'] as const).map((codeType) => {
const rY = y;
const y0 = scale(stages[codeType]);
y += y0;

return (
<Rect
key={codeType}
{...(props as any)}
x={(props.x ?? 0) - ((props.barWidth as number) ?? 0) / 2}
y={rY}
width={props.barWidth}
height={y0}
fill={toDisplay(codeType).color}
/>
);
})}
</>
);
};

type TickProps = { text?: string } & VictoryLabelProps;

const Tick = ({ text, ...props }: TickProps) => {
Expand Down
Loading

0 comments on commit f485f36

Please sign in to comment.