Every marker on this page belongs to some part of you. Pick a region on the figure, or a system from the list, to see what the watch, the lab and the genome each have to say about it.
Daily values with a 7-day average through them. One note before you read the sleep chart: I charge my Apple Watch overnight most nights, so sleep is recorded on a minority of them, and that minority leans toward disrupted nights rather than typical ones. It is the least trustworthy series on this page.
Every stored value for the selected range, newest first. The same data the charts draw from.
Exercise pulled out of the general health metrics, because training load is a thing you steer week to week rather than a number you watch drift. Everything here comes from closed Apple Watch workouts and exercise minutes.
The same fixed list of pairs, tested every time. What is drawn is the 95% confidence interval, not just the estimate, because that is the part that separates a real relationship from a coincidence: if the bar crosses the zero line, I cannot tell it apart from nothing at all. Most of them do. Two further discounts are applied before anything gets called a finding: one for how much each day repeats the day before it, which is a lot, and one for the slow drift of the whole record, since two numbers that rose over the same two years will correlate whether or not they are connected. The ⓘ on each row explains what that row compares and shows its working. Nothing here rests on my sleep data, which used to be half this list: I charge the watch overnight most nights, so those rows were built on a small and unrepresentative set of nights and were never worth the space.
Resting heart rate, HRV, breathing rate and wrist temperature tend to move together when the body is under load it did not choose: an infection, a bad week, too much training. This flags stretches where at least two of them broke away from my own 60-day baseline at once. It is pattern-spotting after the fact, not a diagnosis, and it cannot tell one cause from another.
Transcribed by hand from lab PDFs. Three draws so far, across two labs, which is why a few reference ranges shift between the first panel and the two after it. The shaded band on each chart is the lab's own reference range; the one exception is LDL, where the widely used desirable threshold of 100 is shaded instead of the lab's more permissive 130. I also take 7.5 g of creatine a day, which the kidney markers cannot be read without. The ⓘ on each of those three says why.
A phone that refuses to answer questions, a key/value store, and one HTML file. No database, no framework, no build step. If you only came for the numbers you can stop here. This is the part where I explain the plumbing to nobody in particular. If you would rather build one of these than read about mine, there is a step by step guide: what it costs, what to install, and the four mistakes that cost me the most time.
There is no API for Apple Health. Not a locked one, not a paid one. None.
HealthKit lives on the phone, in an encrypted store, and nothing on a server can reach in and ask it anything. Not with my password, not with an OAuth token, not at all. Apple made that call deliberately and I think they were right, which is inconvenient of them.
So the data can't be pulled. It has to be pushed, by the phone, on a schedule it decides. Every other decision here is downstream of that one fact: why there's an ingest endpoint at all, why it has to be idempotent, why a missing day means "the phone didn't call" rather than "nothing happened".
An iPhone app reads whatever HealthKit will hand over, wraps it in JSON and POSTs it to a serverless function that exists for the few hundred milliseconds it takes to run. That function writes to a key/value store I never had to configure. The page fetches the whole history back as one blob and draws every chart in the browser.
Total infrastructure: zero servers, zero databases, zero dollars a month. The most complex moving part is a phone automation with a toggle I got wrong for an entire evening (see below).
This is the one piece I'd be properly annoyed to get wrong, so it is the dullest code in the project. Four rules:
Workouts arrive carrying routes. GPS traces. Where I run, when, how often, starting from my front door, which is a fairly complete description of where I live and when I'm not in it.
None of it is written. The normalizer reduces a workout to three numbers (count, minutes, energy) and drops everything else on the floor. There's a test that feeds it a payload stuffed with location data and asserts none of it survives, because "I'm fairly sure I handled that" is not a security model for a page anyone on the internet can read.
index.html is around 2,900 lines and imports nothing. No React, no D3, no
bundler, no npm install to deploy. Every chart on this page is SVG assembled
by hand from the numbers.
That isn't asceticism, it's laziness with a long time horizon. I wanted to open one file in a year and understand all of it, and I wanted deploying to be "drag the folder, done". Colors are a palette checked for color-vision deficiency at every pair, and no status anywhere is signaled by color alone.
Workouts and health metrics sync on separate schedules, so days regularly landed with
workouts attached but no exercise-ring reading. The streak logic read
exerciseMinutes ?? 0, scored those days as zero minutes, and broke a
three-year streak on what was actually a sync gap.
The fix wasn't a better number, it was a third state. Every day is now worked, rest or unknown, and only a real ring reading can prove a short day. Missing data now looks like missing data everywhere on the page, which is the whole point.
Dates were built at UTC midnight and then formatted in the reader's own timezone. In New York that lands on 7pm the previous evening. Every label on the page (axes, tooltips, the streak, the lab dates) was silently off by one, and nothing looked broken.
I found it only because the streak tile said "since Jan 18" while the underlying record said Jan 19. These are calendar days, not instants, so they're now formatted in UTC, with tests that run under five timezones and check a year boundary specifically.
The body map above took four goes. Hand-authored SVG paths: the legs read as stilts. Second attempt: a "waistband" seam where the torso met the legs. Third attempt, and I want to be clear that this happened: a signed-distance-field raymarcher in numpy, which fused both thighs into a skirt and rendered every muscle groove as a machined trench.
Then I gave up and used an MIT-licensed anatomical asset, which took about ten minutes and looks like a human being. Knowing when to stop building something is a skill. I am still working on it.
125 checks across four suites, covering the normalizer, the ingest endpoint, the training
logic and the three derived sections. The front-end suites do something slightly unusual:
rather than copying the functions, they parse index.html, slice the real
source out of the script block by bracket-matching, and evaluate that. A copied test
happily passes while the live page is broken. This one can't.
I also test the tests. Flip a threshold, delete a guard, scramble a lookup table, then confirm the suite actually goes red. A test that still passes on deliberately broken code is worse than no test at all, because you believe it.
Not the code. Loading three years of history. Health Auto Export has a Batch Requests toggle, off by default, and for an hourly sync it should stay off. For a bulk backfill it has to be on, or the export fails with an empty response box and an error message containing no information whatsoever.
That cost me an evening and roughly forty attempts, and the answer was one switch. The
npm run check script exists entirely because of it: it reports coverage per
year and per automation, so I can see which of the two is behind instead of
guessing.
None of this is clever. It's a phone, a JSON file and one page. But it runs itself, costs nothing, and every number on it traces back to the thing that measured it, which is more than I can say for most health dashboards I've paid for.