Point GitHub's streak grid at your own spreadsheet
Quilt turns any CSV with a date column into the GitHub contribution graph you already know how to read: one command, one SVG, no account, no server.
GitHub's contribution graph might be the most-read chart in a developer's day. A single glance tells you rhythm, streaks, and the week you clearly went on vacation. It works because the shape is so legible: 52 columns of 7 squares, darker means busier, done. The catch is that it only ever draws one thing (your commits) from one place (GitHub).
Quilt points that exact grid at any CSV you own. Gym sessions, meditation minutes, pages read, days sober, support tickets, sensor readings: if a column has dates in it, you get the green squares. One command, one SVG file, no account and no server.
habit-heatmap workouts.csv -o heatmap.svg --value-col minutes --label "Workout minutes"
Two functions with a dict in the middle
The whole library is really two functions, and the interesting part is the seam between them.
load_events reads a CSV and aggregates rows per calendar day. Point it at your date column
and it counts rows per day; hand it a numeric column too and it sums that instead. What comes
back is deliberately boring: a plain dict[date, float]. That's the entire contract.
render_svg takes that dict and nothing else conceptual, and lays out the week grid as a
self-contained SVG string.
Keeping a plain dictionary in the middle is the design decision that makes the rest fall out
for free. Parsing and rendering never touch each other, so each is testable on its own. And
because the renderer only wants a dict[date, float], your data doesn't have to come from a
CSV at all. A database cursor, an API response, a generator: anything that yields dated rows
can feed the same grid.
from habit_heatmap import load_events_from_rows
counts = load_events_from_rows(db.query("SELECT logged_at AS date, minutes FROM sets"))
The core does all of this with zero runtime dependencies: the CSV parser and SVG renderer are
pure stdlib. PNG export exists, but it's an opt-in extra that pulls in cairosvg, so nobody
who just wants an SVG pays for a rasterizer they'll never call.
The color scale is relative on purpose
The one design choice I'd defend hardest is that the color scale is relative, not absolute. The darkest cell always marks the busiest day in the range you rendered. A person who meditates ten minutes a day and a person who runs marathons get charts that are equally legible, instead of the casual user's heatmap coming out uniformly pale because their "a lot" is someone else's warm-up. Your best day is dark; everything else is shaded against it.
The gotcha that cost me the most is time zones. A workout you logged at 11pm Central, exported
as a UTC timestamp, is tomorrow in UTC, so it lands in the wrong square and quietly splits a
streak. Quilt takes a --tz flag that rebuckets timestamps into your own day boundaries
before it counts anything, which turns "why is my Tuesday empty" into a non-event. Bare dates,
MM/DD/YYYY, ISO 8601 timestamps: all parse without configuration, so most real-world exports
just work.
By default the grid spans exactly the dates present in your data, not a forced trailing year,
so a three-week experiment renders as three weeks. There are --start/--end flags for the
"always show the last full year" case, but nothing is assumed.
Try it
Grab any spreadsheet with a date column, export it to CSV, and run
habit-heatmap yourfile.csv -o heatmap.svg. Add --value-col if you want intensity and
--theme blue (or purple, mono, dark) if green isn't your color. Open the SVG, and
there's your year, stitched together. The repo has
a cookbook that turns git history, habit-app exports, and time logs into the same picture.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…