GLP-1 Plotter
The itch
People on a GLP-1 medication like tirzepatide often manage their own titration. They step a dose up or down over weeks, and many dial partial doses from a multi-dose KwikPen by counting clicks. The existing tool for seeing what that plan does to levels in the body was glapp.io's plotter, and it had a scheduling bug: a one-week step could produce two doses. It also had no notion of pens, so it couldn't say how many clicks were left in one. The plotter started as a chart built in a Claude chat and grew into a replacement that keeps glapp's maths, fixes the schedule and treats pens as a main path.
What it does
A plan is one medication, one dosing interval and a list of steps, each a dose and a number of doses. A zero dose is a pause. The chart plots the estimated milligrams in the body over time, shaded by dose level, and a summary strip shows today's level, the next dose and the peak.
Switching to KwikPen clicks puts pens above the steps. Each pen has a strength, its own steps and a meter showing clicks and milligrams left. One labelled dose is 60 clicks, so mg = clicks / 60 × strength, and a pen holds 240 clicks. Its "golden dose" switch counts the extra fifth dose in the pen, for 300.
Plans can be shared as a link, exported and imported as JSON, or exported as a PNG chart or a PDF with every dose listed. Everything runs in the browser and saves to local storage, with no account or server. Every page and export carries the same disclaimer: the curve is an estimate from a population model, not a measurement, and the tool never recommends a dose.
How it works
The model is a one-compartment pharmacokinetic model ported exactly from glapp, so results compare directly:
Each drug has a half-life, a bioavailability, a volume of distribution and a time to peak. The absorption rate isn't given directly, so it's solved from the time to peak with Newton's method. Each dose then adds a Bateman absorption curve, the doses are summed, and the total is sampled every six hours. The maths lives in one pure JavaScript module with no DOM, which means the page and the Node test suite load the same code. The tests pin the constants, down to tirzepatide's absorption rate of about 3.3122, so a design change can't drift the numbers.
The page is React with Vite, Tailwind, shadcn/ui and Recharts, served from GitHub Pages. There's no CI build, so the built output is committed alongside the source.
What got hard along the way
The first version counted steps in weeks, the way glapp does, and a one-week step on a weekly schedule could land on two dose days. Steps are now counted in doses, laid out back to back, so dose k of a plan always falls on day k times the interval. Saved plans and share links in the old shape still exist, so an upgrade function converts them on load, and it has to keep working.
Recharts 3 caused a quieter bug. It orders chart layers by when they mounted, not by their order in the JSX, so a layer added after the first render could paint over the curve. The dose-level bands needed an explicit z-index to stay underneath.
The design was settled one comparison at a time. For every visible change, two or three working options shared one layout and differed in a single variable, tested at phone widths with real data, and one got picked by letter. That's how the chart came to be coloured by dose level rather than by step, with each dose keeping one colour everywhere. It's also why pens became a quiet row under the dates instead of dashed lines across the plot. The phone layout for a pen's steps took the longest: three rounds of options were rejected before the fourth stuck.
Where it stands
The plotter is live on GitHub Pages under an MIT licence. It supports tirzepatide, injected and oral semaglutide, and retatrutide. Two offered features haven't been taken up yet: a warning before a share link replaces a saved plan, and undo for removing a step or a pen.