Your activity heatmap: what your timing says
The 7×24 day-hour grid — which events feed each cell, local-time bucketing, the own-actions-only rule, the golden-hour and persona ring stats, and the limit that matters most: timing shows when, never why.
A heatmap of your own activity is the closest a data export gets to a portrait:
seven rows of days, twenty-four columns of hours, one number per cell — and somehow
it looks like you. Before you read anything into it, though, it is worth knowing
exactly which events light each cell, whose clock decides what "11pm" means, what
the drill-down actually lists, and the one rule that keeps the whole picture honest:
this grid shows when, and the word "why" is not in its vocabulary.
The heatmap lives on the Activity page (with the full record feed underneath),
mirrored as a card on the Dashboard, and every cell is a link: click one and the
drill-down lists the exact events behind it, timestamp by timestamp.1
Everything computes in your browser from the archive you loaded — no server
participates in any of this.2
The heatmap is a 7-day × 24-hour histogram in your device's local time: every
timestamped action you performed is bucketed by getDay() × getHours() of its
instant, cell by cell, with undated rows dropped rather than guessed.3 On the
Activity page only your own actions count — other people's messages to you are
filtered out by sender, so a chatty friend cannot make your grid look busy.4
You can focus the map through 27 activity modes across six always-visible
categories (Engagement, Content, Stories, Searches, Browsing, Chats & Network),
and any cell opens a drill-down listing that hour's events. Around the grid sit
derived views of the same timestamps: the "golden hour" card (your single busiest
hour and its share of everything), hourly and weekday rhythm charts, and a persona
ring — Daybreak soul, Midday mover, Evening socialite, Night owl — from fixed
band boundaries. The honest limit is structural: a cell proves when recorded
actions happened, never what they meant, never what you should do next, and never
a claim about time outside the export's window.
- 1minhow one cell is computed, and whose clock it uses.
- 3minthe own-actions rule, the modes, and the drill-down.
- 5mingolden hour, rhythm charts, the persona ring, and what timing cannot say.
How one cell is computed
The core is deliberately small: an empty 7×24 grid, then one pass over
timestamps.3
- Buckets — seven weekday rows (Sunday = 0, matching JavaScript's
getDay(),and every heatmap in the app uses the same convention) × 24 hour columns.
- The instant — each record's unix-seconds timestamp is converted to a date in
your device's timezone and filed at
[day][hour]. No rounding beyond the
integer hour, no offset math, no smoothing. - Drops — a timestamp of
0or less (the parser's "could not read this"marker) is skipped entirely. Missing data makes a cell quieter, never busier —
the same null-not-zero discipline the counting post covers for medians.5 - The scale — cell alpha ramps from 25% to 100% of the grid's maximum, so the
busiest hour is full colour and quiet hours are a whisper of it. Empty cells stay
faint rather than looking like zeroes of importance.
Illustrative. Three actions carry timestamps on a Tuesday: a like at 08:14,
a comment at 08:52, a message you sent at 21:03 (device local time). Cell[2][8] reads 2, cell [2][21] reads 1, everything else that day reads 0 — the
grid never interpolates between them, never smooths Tuesday into Wednesday, and
never guesses where an undated row might have gone. A second like arriving with a
broken timestamp changes nothing: it is skipped, not placed.3
The same instant, stored as UTC by the export, is what the FAQ's timezone answer
describes: the underlying moment never changes; the display timezone is your
device's, which is why an event near midnight can appear to move a day when you
travel.6 The heatmap is honest about which clock it draws — the Dashboard card
says "local time" in its own title.7
Whose actions count — the rule that keeps it yours
The Activity page's grid has a filter the Dashboard's does not: your actions
only.4 Messages enter the histogram only when the sender is your own
account; follows count only when you followed; watched reels, posts, comments,
stories, searches and the rest are yours by construction — the collector's contract
is literally "every timestamped event performed BY THE USER".
That rule has a visible counterpart: the card reads "Only your own actions are
counted — never anyone else's", and the drill-down repeats it — "other people's
messages are never counted".8 A conversation is two people's timestamps;
this grid shows one side of it, deliberately, because the question the page answers
is about your rhythm, not your inbox's volume.
Worth knowing: the Dashboard's compact grid is a different feed — it captions
itself "all activity" and includes follow events (people following you arriving as
timestamps), so its cells can light up from other people's actions.7 Same
math, different membership. Read the Activity page as the authoritative "when did I
do things" map.
What feeds the collector
The mode chips are not cosmetic groupings — each one is a typed slice of the same
event sweep, and knowing which sources exist tells you what the grid cannot
show:9
- Engagement and Content — posts, reels, comments and likes you gave, from the
content and activity sections of the export, each carried with its own
timestamp. - Stories — your story activity timestamps.
- Searches — your search history entries with their recorded times.
- Browsing — watch history: reels you viewed, when the export recorded it.
- Chats & Network — messages you sent (the sender filter above), plus
following actions from your connections list when they carry a timestamp.
Every source obeys the same two admission rules: a numeric timestamp greater than
zero, and an actor that is you. That is why the grid has no tab for "things that
happened to your account" — follows-you-received, incoming messages and mentions
are different record types, and on the Activity page they are not in the building.
A cell, in other words, is a count of your recorded deeds at a weekday-hour —
and a deed without a readable time does not become a deed at an approximate
one.
What sits around the grid
All of these read the same timestamps — nothing is measured twice:
- Golden hour. Your single busiest day-hour cell, its event count, and its
percentage of everything in the export.10 The percentage is a plain share of
the total recorded actions — arithmetic you could redo yourself from the record
feed, which is the point of printing it rather than a decorative "peak score". - Rhythm charts. Hourly and weekday aggregations with segmented toggles, plus
a "busiest times" summary — a compact sibling of the counting post's top-five
posting-times table, but over all activity rather than posts alone.11 - The persona ring. Four fixed bands — dawn 05–11, day 11–17, evening 17–23,
night otherwise — labelled Daybreak soul / Midday mover / Evening socialite /
Night owl.12 A descriptive bucket of where your timestamps cluster, not a
personality claim; it changes if your habits change, because it is your habits. - 27 activity modes. Six categories, always visible, each chip refocusing the
grid: Engagement, Content, Stories, Searches, Browsing, Chats & Network. The
posting heatmap on the Content page is yet another slice — posts and reels
only.9 - The drill-down.
/heatmap/[day]/[hour]lists that cell's events with typechips and timestamps, and only ever for actions matching the current scope.1
This is what makes the heatmap checkable rather than merely pretty: a suspicious
bright cell is one click from its evidence — the exact rows, their types and
their seconds-since-epoch rendered as times. If the list and the cell ever
disagreed, you would see it immediately; they cannot disagree, because the cell
is a count over that list. The same pattern as everything else in this
product: the summary exists to make the raw record easier to reach, not to
replace it.
What timing cannot say
This is the part the plan for this post insisted on, and the code agrees with it
in three separate places:
- Timing ≠ meaning. A 2am cluster means you were awake at 2am and did things.
It does not mean insomnia, a crisis, a job or a mood. The file contains
timestamps — the interpretation belongs to you, and a grid cannot borrow it. - Never a recommendation. The posting panels say so in-product: your heatmap
shows "your own posting habit, never a recommendation about when to post".13
An export is a record of what you did, not an experiment with a control group;
there is no "best time" hiding in your own history, only your times. - Window, not life. Every cell is bounded by the export's date range. Quiet
edges are the window's edges — the streaks and gaps caveats apply here too:
"the window's edges are not silence".14 And rows without readable
timestamps never enter the grid at all: the FAQ's rule is to show "Unknown"
rather than invent a date, which means timing views are built only from rows
that earned a real one.
Why did cells near midnight move a day when I travelled?
Because the export stores instants in UTC and the heatmap buckets them in your
device's current timezone — the instant is identical, the label follows your
clock. The FAQ documents exactly this: the date can shift for events close to
midnight; the underlying timestamp never changed.
Why is my heatmap mostly empty?
Three ordinary reasons: the export window is short (a "last 3 months" request
draws one quarter of a life), undated rows are skipped rather than guessed, and
the Activity page counts only your own actions — passive consumption without
recorded timestamps leaves no cell. Switch modes to check; each chip has its own
event list.
Does a bright cell mean I was addicted that hour?
It means recorded actions happened then. Whether that was joy, work, insomnia or
a boring week is context the file does not carry — timing is the one dimension an
export measures perfectly and the one dimension that explains nothing on its own.
The grid is a mirror with a clock, not an evaluation.
Why are whole stretches missing — including things I remember doing?
Three filters, all documented elsewhere in the app: rows whose timestamp did not
parse are shown as "Unknown" in the record views and skipped in timing views (the
product's rule is to invent no dates at all); actions outside the export's date
window are not in the file to begin with; and on the Activity page, actions other
people performed for you are excluded by the own-actions rule. A sparse grid is
usually a narrow window or an undated batch, not a quiet year — check the
record feed underneath the heatmap for rows marked Unknown before believing the
blank.
Two heatmaps disagree — the Dashboard and the Activity page. Which is right?
Both; they count different sets. The Activity page counts only actions you
performed (messages filtered to your own sends). The Dashboard's compact card says
"all activity" and includes events like new followers arriving — other people's
actions as timestamps. For "when was I active", the Activity page is the answer.
Can I click through to the underlying events?
Yes — every cell links to the drill-down for that day and hour, which lists the
timestamped events by type. The same guarantee as everywhere else: the list is
assembled from your archive in the browser, and the drill-down's own header notes
that other people's messages are never in it.
Questions this comes up
The activity post for what the underlying record contains; the counting post for
the streak, gap and top-time definitions these timestamps also feed; the
year-in-review post for the same events read by month instead of by hour.
1: components/ActivityHeatmap.tsx (cells are links with the mode's scope)
and views/HeatmapActivity.tsx — the /heatmap/[day]/[hour] route lists the
hour's events with type chips and timestamps.
2: views/Activity.tsx — the grid and every derived card are built in
component memory from the loaded archive; no network call exists in the path.
3: ActivityHeatmap.tsx buildGrid — empty 7×24 array; t <= 0 skipped;grid[d.getDay()][d.getHours()]++ on the unix-seconds timestamp (local time).
4: ActivityHeatmap.tsx collectActivityEvents — event contract "performed
BY THE USER"; the messages loop keeps only sender === archive.personal.username.
5: The counting post's null-not-zero section — undated rows are absent
data, never zeroes.
6: FAQ timezone answer — instants stored as unix seconds, rendered in local
timezone; the moment is identical, the display can cross midnight.
7: views/Dashboard.tsx — card title "Activity heatmap (all activity, local
time)"; its feed includes followers/following timestamps alongside your own posts,
reels and stories.
8: views/Activity.tsx — "Only your own actions are counted — never
anyone else's."; HeatmapActivity.tsx — "other people's messages are never
counted".
10: views/Activity.tsx — golden-hour card: busiest cell, its count and its
share of all actions, plus the supporting stat row.
11: views/Activity.tsx — "Hourly rhythm / By weekday" segmented charts
and the busiest-times foot line; adjacent to getBestPostingTimes-style
aggregations over the same instants.
12: views/Activity.tsx — persona ring bands dawn 05–11 / day 11–17 /
evening 17–23 / night otherwise, with the four labels.
9: ActivityHeatmap.tsx — 27 non-all modes in HEATMAP_MODES across the
six listed categories; the Content page's posting heatmap is posts+reels only.
13: views/Insights.tsx — "This is your own posting habit, never a
recommendation about when to post."
14: views/Insights.tsx — "A deleted post still counts as a gap in the
timeline, and the window's edges are not silence."
Footnotes
- drill
- local
- grid
- own
- nulls
- tz
- dash
- strings
- modes
- hero
- rhythm
- ring
- norec
- edges