Skip to content

Commit

Permalink
Merge pull request #21 from TomPlum/develop
Browse files Browse the repository at this point in the history
Minor miscellaneous graph improvements
  • Loading branch information
TomPlum authored Nov 4, 2024
2 parents 529c117 + e8c2bfb commit ff20995
Show file tree
Hide file tree
Showing 27 changed files with 359 additions and 109 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

A simple 2D line chart visualisation of my sleep data as recorded by my Apple Watch using [Pillow](https://pillow.app/) for iOS.

# Examples
- [Recent sleep quality vs awake time stacked charts](https://tomplum.github.io/sleep?metric=awake_time&start=1722911767000&end=1728182167000&lng=en&stacked=false&metrics=quality%2Cawake_time)
- [Sleep quality over time across all recorded sessions](https://tomplum.github.io/sleep?metric=quality&start=1534457817000&end=1728199961000&lng=en&stacked=false&metrics=quality%2Cdeep_sleep)

# Sleep Quality (%)
![quality.png](docs/images/quality.png)

Expand All @@ -24,6 +28,7 @@ A simple 2D line chart visualisation of my sleep data as recorded by my Apple Wa

- Select multiple sleep metric at once
- A split view that renders stacked charts to compare two metric with a brush
- Remove duplicate improvements made label?
- Add brush in the middle
- Split SleepContext into two. Isolate configuration into its own context
- Split SleepContext into two. Isolate configuration into its own context
- Chrome performance?
- Favicon not showing in Chrome/Safari?
Binary file added public/Pillow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 38 additions & 7 deletions public/PillowData.csv → public/PillowData-02-11-24.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2377,8 +2377,8 @@ Optional(2024-07-24 22:36:29 +0000), Optional(2024-07-25 06:46:07 +0000), 490, N
Optional(2024-07-25 22:14:34 +0000), Optional(2024-07-26 06:20:16 +0000), 486, No, 58, 167, 75, 179, 65, 0, Undefined,
Optional(2024-07-27 02:45:48 +0000), Optional(2024-07-27 08:31:40 +0000), 346, No, 37, 93, 80, 142, 32, 0, Undefined,
Optional(2024-07-27 23:42:24 +0000), Optional(2024-07-28 07:25:03 +0000), 463, No, 58, 147, 76, 173, 66, 0, Undefined,
Optional(2024-07-28 22:09:58 +0000), Optional(2024-07-29 06:06:09 +0000), 476, No, 63, 112, 89, 233, 42, 0, Undefined,
Optional(2024-07-29 22:53:58 +0000), Optional(2024-07-30 06:06:05 +0000), 432, No, 64, 117, 83, 172, 60, 0, Undefined,
Optional(2024-07-28 22:09:58 +0000), Optional(2024-07-29 06:48:20 +0000), 518, No, 62, 112, 89, 275, 42, 0, Undefined,
Optional(2024-07-29 22:53:58 +0000), Optional(2024-07-30 07:05:01 +0000), 491, No, 68, 117, 83, 231, 60, 0, Undefined,
Optional(2024-07-30 22:55:09 +0000), Optional(2024-07-31 06:20:07 +0000), 445, No, 49, 112, 43, 272, 18, 0, Undefined,
Optional(2024-08-01 00:32:03 +0000), Optional(2024-08-01 07:00:04 +0000), 388, No, 57, 87, 64, 205, 33, 0, Undefined,
Optional(2024-08-02 00:04:03 +0000), Optional(2024-08-02 07:00:04 +0000), 416, No, 48, 143, 32, 171, 70, 0, Undefined,
Expand Down Expand Up @@ -2447,11 +2447,42 @@ Optional(2024-09-28 02:20:47 +0000), Optional(2024-09-28 08:16:17 +0000), 356, N
Optional(2024-09-28 13:19:19 +0000), Optional(2024-09-28 15:54:05 +0000), 155, Yes, 78, 0, 45, 95, 15, 0, Undefined,
Optional(2024-09-28 22:35:49 +0000), Optional(2024-09-29 06:56:24 +0000), 501, No, 86, 49, 120, 252, 79, 0, Undefined,
Optional(2024-09-29 13:03:36 +0000), Optional(2024-09-29 14:33:59 +0000), 90, Yes, 84, 0, 15, 64, 11, 0, Undefined,
Optional(2024-09-30 00:17:58 +0000), Optional(2024-09-30 06:15:05 +0000), 357, No, 88, 19, 89, 116, 133, 0, Undefined,
Optional(2024-09-30 00:17:58 +0000), Optional(2024-09-30 06:15:05 +0000), 357, No, 88, 19, 89, 116, 133, 0, OK,
Optional(2024-09-30 21:50:05 +0000), Optional(2024-10-01 06:07:09 +0000), 497, No, 85, 68, 123, 213, 92, 0, OK,
Optional(2024-10-01 21:50:06 +0000), Optional(2024-10-02 05:42:43 +0000), 473, No, 93, 17, 95, 197, 165, 0, OK,
Optional(2024-10-02 22:30:20 +0000), Optional(2024-10-03 05:18:59 +0000), 409, No, 91, 18, 83, 210, 99, 0, OK,
Optional(2024-10-03 21:36:38 +0000), Optional(2024-10-04 06:02:16 +0000), 506, No, 84, 42, 118, 256, 90, 0, Undefined,
Optional(2024-10-05 02:49:31 +0000), Optional(2024-10-05 09:34:08 +0000), 405, No, 88, 31, 80, 162, 131, 0, Undefined,
Optional(2024-10-05 16:36:13 +0000), Optional(2024-10-05 18:39:35 +0000), 123, Yes, 90, 0, 26, 63, 34, 0, Undefined,
Optional(2024-10-06 02:36:07 +0000), Optional(2024-10-06 07:32:41 +0000), 297, No, 74, 17, 59, 120, 100, 0, Undefined,
Optional(2024-10-03 21:36:38 +0000), Optional(2024-10-04 06:02:16 +0000), 506, No, 84, 42, 118, 256, 90, 0, OK,
Optional(2024-10-05 02:49:31 +0000), Optional(2024-10-05 09:34:08 +0000), 405, No, 88, 31, 80, 162, 131, 0, OK,
Optional(2024-10-05 16:36:13 +0000), Optional(2024-10-05 18:39:35 +0000), 123, Yes, 90, 0, 26, 63, 34, 0, OK,
Optional(2024-10-06 02:36:07 +0000), Optional(2024-10-06 07:32:41 +0000), 297, No, 74, 17, 59, 120, 100, 0, OK,
Optional(2024-10-06 22:38:15 +0000), Optional(2024-10-07 06:45:34 +0000), 487, No, 89, 29, 109, 246, 103, 0, OK,
Optional(2024-10-07 22:50:05 +0000), Optional(2024-10-08 06:36:38 +0000), 467, No, 85, 13, 168, 219, 66, 0, OK,
Optional(2024-10-08 23:06:16 +0000), Optional(2024-10-09 06:13:36 +0000), 427, No, 94, 0, 122, 199, 106, 0, Good,
Optional(2024-10-09 21:49:54 +0000), Optional(2024-10-10 06:14:05 +0000), 504, No, 88, 46, 130, 242, 87, 0, OK,
Optional(2024-10-10 23:07:22 +0000), Optional(2024-10-11 06:47:43 +0000), 460, No, 90, 44, 142, 158, 117, 0, OK,
Optional(2024-10-12 01:47:36 +0000), Optional(2024-10-12 07:16:18 +0000), 329, No, 72, 32, 85, 159, 54, 0, OK,
Optional(2024-10-12 23:34:48 +0000), Optional(2024-10-13 07:27:20 +0000), 473, No, 90, 18, 76, 259, 120, 0, OK,
Optional(2024-10-13 23:07:57 +0000), Optional(2024-10-14 05:58:39 +0000), 411, No, 93, 16, 88, 158, 148, 0, OK,
Optional(2024-10-14 22:09:29 +0000), Optional(2024-10-15 07:00:10 +0000), 531, No, 94, 11, 141, 269, 110, 0, OK,
Optional(2024-10-15 22:35:11 +0000), Optional(2024-10-16 07:06:22 +0000), 511, No, 84, 9, 45, 304, 153, 0, OK,
Optional(2024-10-17 00:51:24 +0000), Optional(2024-10-17 07:32:20 +0000), 401, No, 83, 28, 60, 222, 91, 0, OK,
Optional(2024-10-18 12:52:47 +0000), Optional(2024-10-18 14:12:47 +0000), 80, Yes, 90, 0, 13, 37, 30, 0, OK,
Optional(2024-10-19 00:07:23 +0000), Optional(2024-10-19 07:46:01 +0000), 459, No, 86, 25, 75, 248, 111, 0, OK,
Optional(2024-10-19 14:48:32 +0000), Optional(2024-10-19 16:47:23 +0000), 119, Yes, 77, 14, 0, 77, 28, 0, OK,
Optional(2024-10-19 22:37:55 +0000), Optional(2024-10-20 06:33:50 +0000), 476, No, 83, 11, 77, 318, 70, 0, OK,
Optional(2024-10-20 15:52:27 +0000), Optional(2024-10-20 17:18:07 +0000), 86, Yes, 89, 0, 10, 61, 14, 0, OK,
Optional(2024-10-20 23:33:04 +0000), Optional(2024-10-21 06:42:49 +0000), 430, No, 93, 0, 108, 232, 90, 0, OK,
Optional(2024-10-21 23:11:10 +0000), Optional(2024-10-22 06:45:15 +0000), 454, No, 93, 20, 125, 222, 88, 0, OK,
Optional(2024-10-22 22:34:21 +0000), Optional(2024-10-23 05:37:55 +0000), 424, No, 81, 28, 54, 260, 82, 0, Undefined,
Optional(2024-10-23 23:06:24 +0000), Optional(2024-10-24 06:36:06 +0000), 450, No, 60, 92, 36, 245, 76, 0, Bad,
Optional(2024-10-25 00:07:28 +0000), Optional(2024-10-25 06:42:29 +0000), 395, No, 70, 60, 94, 187, 54, 0, Bad,
Optional(2024-10-25 23:03:25 +0000), Optional(2024-10-26 08:50:05 +0000), 587, No, 80, 79, 99, 281, 128, 0, OK,
Optional(2024-10-27 00:23:09 +0000), Optional(2024-10-27 09:22:51 +0000), 540, No, 88, 43, 104, 293, 99, 0, OK,
Optional(2024-10-27 23:32:22 +0000), Optional(2024-10-28 07:03:32 +0000), 451, No, 94, 0, 99, 235, 117, 0, OK,
Optional(2024-10-28 23:35:03 +0000), Optional(2024-10-29 07:45:40 +0000), 491, No, 95, 13, 164, 191, 123, 0, OK,
Optional(2024-10-29 23:19:04 +0000), Optional(2024-10-30 07:58:47 +0000), 520, No, 89, 17, 86, 297, 120, 0, OK,
Optional(2024-10-30 17:11:22 +0000), Optional(2024-10-30 18:00:12 +0000), 49, Yes, 73, 0, 0, 49, 0, 0, OK,
Optional(2024-10-31 00:23:42 +0000), Optional(2024-10-31 08:09:01 +0000), 465, No, 95, 17, 118, 205, 125, 0, Undefined,
Optional(2024-10-31 23:49:03 +0000), Optional(2024-11-01 07:45:06 +0000), 476, No, 95, 0, 162, 179, 135, 0, Undefined,
Optional(2024-11-02 02:49:59 +0000), Optional(2024-11-02 11:35:00 +0000), 525, No, 94, 0, 166, 201, 157, 0, Undefined,
Optional(2024-11-02 16:04:23 +0000), Optional(2024-11-02 16:34:33 +0000), 30, Yes, 77, 0, 0, 27, 3, 0, Undefined,
75 changes: 19 additions & 56 deletions src/context/SleepContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { SleepContext } from 'context/SleepContext'
import { PropsWithChildren, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'
import { PropsWithChildren, useEffect, useMemo } from 'react'
import { SleepContextBag } from 'context/types'
import { useSleepData } from 'data/useSleepData'
import { useQueryParams } from 'hooks/useQueryParams'
import { SleepMetric } from 'modules/controls/MetricConfiguration'
import dayjs from 'dayjs'
import { useSleepGraph2DData } from 'modules/graph/hooks/useSleepGraph2DData'
import { PageRoutes } from 'routes'
import { useTranslation } from 'react-i18next'
import { useDefaultQueryParams } from 'hooks/useDefaultQueryParams'

export const SleepContextProvider = ({ children }: PropsWithChildren) => {
const { i18n } = useTranslation()
const { sleepData, loading } = useSleepData()
const { queryParams: { start, end, metric, lng, stacked, metrics }, updateQueryParam } = useQueryParams()

const [language, setLanguage] = useState(lng)
const [rangeEnd, setRangeEnd] = useState(end)
const [rangeStart, setRangeStart] = useState(start)
const [currentMetric, setCurrentMetric] = useState(metric)

const [stackedView, setStackedView] = useState(stacked)
const [stackedMetrics, setStackedMetrics] = useState(metrics)
const {
currentMetric,
rangeStart,
rangeEnd,
language,
stackedView,
stackedMetrics,
setCurrentMetric,
setRangeEnd,
setRangeStart,
setStackedView,
handleSetStackedMetrics
} = useDefaultQueryParams({
loading,
sleepData
})

const sleepGraphData2d = useSleepGraph2DData({
sessions: sleepData?.sessions ?? [],
Expand All @@ -34,55 +40,12 @@ export const SleepContextProvider = ({ children }: PropsWithChildren) => {
return date.getFullYear() === 2024 && date.getMonth() === 8 && date.getDate() === 6
})?.date

// TODO: Move to another hook and cleanup
useEffect(() => {
if (!loading && sleepData && (!rangeStart || !rangeEnd || !currentMetric || !lng || stackedView === undefined || !stackedMetrics)) {
const selectedMetric = currentMetric ?? SleepMetric.QUALITY
setCurrentMetric(selectedMetric)

const selectedStart = rangeStart ?? dayjs(sleepData.latestSession).subtract(2, 'month').toDate()
setRangeStart(selectedStart)

const selectedEnd = rangeEnd ?? sleepData.latestSession
setRangeEnd(selectedEnd)

const selectedLanguage = language ?? 'en'
setLanguage(selectedLanguage)

const selectedStackedView = stackedView !== undefined ? stackedView : false
setStackedView(selectedStackedView)

const selectedStackedMetrics = stackedMetrics ?? []
setStackedMetrics(selectedStackedMetrics)

const params: Record<string, string> = {
metric: selectedMetric,
start: selectedStart.getTime().toString(),
end: selectedEnd.getTime().toString(),
lng: selectedLanguage,
stacked: String(selectedStackedView)
}

updateQueryParam({ route: PageRoutes.SLEEP, params })
}
}, [currentMetric, language, lng, loading, rangeEnd, rangeStart, sleepData, stackedMetrics, stackedView, updateQueryParam])

useEffect(() => {
i18n.changeLanguage(language).then(() => {
console.debug(`Set locale [${language}] from query parameters.`)
})
}, [i18n, language])

const handleSetStackedMetrics = useCallback((setState: SetStateAction<SleepMetric[]>) => {
setStackedMetrics(existing => {
if (typeof setState === 'function') {
return (setState as (existing: SleepMetric[] | undefined) => SleepMetric[])(existing)
}

return setState
})
}, [])

const value = useMemo<SleepContextBag>(() => ({
sleepData,
isSleepDataLoading: loading,
Expand All @@ -99,7 +62,7 @@ export const SleepContextProvider = ({ children }: PropsWithChildren) => {
setStackedView,
stackedMetrics: stackedMetrics ?? [],
setStackedMetrics: handleSetStackedMetrics
}), [sleepData, loading, rangeStart, rangeEnd, currentMetric, sleepGraphData2d, improvementDate, stackedView, stackedMetrics, handleSetStackedMetrics])
}), [currentMetric, handleSetStackedMetrics, improvementDate, loading, rangeEnd, rangeStart, setCurrentMetric, setRangeEnd, setRangeStart, setStackedView, sleepData, sleepGraphData2d, stackedMetrics, stackedView])

return (
<SleepContext.Provider value={value}>
Expand Down
2 changes: 1 addition & 1 deletion src/data/usePillowData/usePillowData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GetPillowDataProps } from 'data/usePillowData/types'

export const usePillowData = ({ type }: GetPillowDataProps) => {
const readFile = useCallback(async () => {
const fileName = type === 'raw' ? 'PillowDataRaw.txt' : 'PillowData.csv'
const fileName = type === 'raw' ? 'PillowDataRaw.txt' : 'PillowData-02-11-24.csv'
const response = await fetch(fileName)

if (!response.ok) {
Expand Down
9 changes: 8 additions & 1 deletion src/data/useSleepData/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface PillowSessionDuration {
rem: number
}

export enum SleepMood {
GOOD = 'good',
OK = 'ok',
BAD = 'bad',
UNKNOWN = 'unknown'
}

export interface PillowSleepSession {
/**
* A unique identifier for the
Expand Down Expand Up @@ -85,7 +92,7 @@ export interface PillowSleepSession {
* myself after waking up. This
* is optionally added some days.
*/
mood?: string
mood: SleepMood
}

export interface PillowSleepData {
Expand Down
12 changes: 6 additions & 6 deletions src/data/useSleepData/useSleepData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { readFileSync } from 'fs'
import { renderHook, waitFor } from '@testing-library/react'
import { wrapper } from 'test'
import { useSleepData } from 'data/useSleepData/useSleepData'
import { SleepDataResponse } from 'data/useSleepData/types'
import { SleepDataResponse, SleepMood } from 'data/useSleepData/types'

describe('Sleep Data (CSV) Parsing Hook', () => {
let pillowData: string
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('Sleep Data (CSV) Parsing Hook', () => {
'endTime': new Date('2018-08-17T05:36:42.000Z'),
'id': 'session-0',
'isNap': false,
'mood': 'OK',
'mood': SleepMood.OK,
'sleepQuality': 56,
'startTime': new Date('2018-08-16T22:16:57.000Z'),
},
Expand All @@ -71,7 +71,7 @@ describe('Sleep Data (CSV) Parsing Hook', () => {
'endTime': new Date('2018-08-18T06:31:27.000Z'),
'id': 'session-1',
'isNap': false,
'mood': 'OK',
'mood': SleepMood.OK,
'sleepQuality': 50,
'startTime': new Date('2018-08-17T21:57:59.000Z'),
},
Expand All @@ -87,7 +87,7 @@ describe('Sleep Data (CSV) Parsing Hook', () => {
'endTime': new Date('2018-08-19T07:30:35.000Z'),
'id': 'session-2',
'isNap': false,
'mood': 'Good',
'mood': SleepMood.GOOD,
'sleepQuality': 72,
'startTime': new Date('2018-08-18T22:53:27.000Z'),
},
Expand All @@ -103,7 +103,7 @@ describe('Sleep Data (CSV) Parsing Hook', () => {
'endTime': new Date('2018-08-20T06:00:39.000Z'),
'id': 'session-3',
'isNap': false,
'mood': undefined,
'mood': SleepMood.UNKNOWN,
'sleepQuality': 56,
'startTime': new Date('2018-08-19T22:38:42.000Z'),
},
Expand All @@ -119,7 +119,7 @@ describe('Sleep Data (CSV) Parsing Hook', () => {
'endTime': new Date('2018-08-21T06:00:21.000Z'),
'id': 'session-4',
'isNap': false,
'mood': 'Good',
'mood': SleepMood.GOOD,
'sleepQuality': 76,
'startTime': new Date('2018-08-20T21:35:34.000Z'),
}
Expand Down
18 changes: 14 additions & 4 deletions src/data/useSleepData/useSleepData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { usePillowData } from 'data/usePillowData'
import { useMemo } from 'react'
import { PillowSleepSession, SleepDataResponse } from 'data/useSleepData/types'
import { useCallback, useMemo } from 'react'
import { PillowSleepSession, SleepDataResponse, SleepMood } from 'data/useSleepData/types'

export const useSleepData = (): SleepDataResponse => {
const { data, isLoading, error } = usePillowData({ type: 'csv' })

const getSleepMood = useCallback((moodValue: string): SleepMood => {
switch (moodValue) {
case 'Good': return SleepMood.GOOD
case 'OK': return SleepMood.OK
case 'Bad': return SleepMood.BAD
case 'Undefined': return SleepMood.UNKNOWN
default: return SleepMood.UNKNOWN
}
}, [])

const sessions = useMemo<PillowSleepSession[]>(() => {
if (!data) {
return []
Expand All @@ -25,7 +35,7 @@ export const useSleepData = (): SleepDataResponse => {
endTime: new Date(record['End Time'].replace('Optional(', '').replace(')', '')),
audioRecordings: Number(record['Amount of audio recordings']),
isNap: record['Is nap'] === 'Yes',
mood: record['Wake-up mood'] === 'Undefined' ? undefined : record['Wake-up mood'],
mood: getSleepMood(record['Wake-up mood']),
sleepQuality: Number(record['Sleep quality']),
duration: {
total: Number(record['Time in Bed (mins)']),
Expand All @@ -42,7 +52,7 @@ export const useSleepData = (): SleepDataResponse => {
const isTooShort = !isNap && duration.total < 90
return hasValidDuration && !hasInvalidBreakdown && hasValidAwakeTime && !isTooShort && !isAllAwakeTime
})
}, [data])
}, [data, getSleepMood])

const { earliestSession, latestSession } = useMemo(() => {
const earliestSession = new Date(Math.min(...sessions.map(session => session.startTime.getTime())))
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useDefaultQueryParams/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useDefaultQueryParams'
6 changes: 6 additions & 0 deletions src/hooks/useDefaultQueryParams/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PillowSleepData } from 'data/useSleepData'

export interface DefaultQueryParamsProps {
loading: boolean
sleepData?: PillowSleepData
}
Loading

0 comments on commit ff20995

Please sign in to comment.