Your Traffic Chart Is Missing a Day, and the API Isn't Lying
Most analytics APIs omit time buckets with no data. Plot the response as-is and the chart quietly closes the gap — a zero day becomes no day at all.
I was building a traffic dashboard for a sim racing team’s site — the owner wanted to see his own numbers without me forwarding screenshots. Simple job: call the analytics API for the last seven days, draw a column per day.
The chart came back with six columns. The axis read Jul 27, Jul 30, Jul 31, Aug 1, Aug 2, Aug 3.
Jul 28 and Jul 29 weren’t late, weren’t loading, weren’t zero. They weren’t there at all — and because the columns were evenly spaced, nothing looked broken. A quiet weekend had rendered as a shorter week.
The mechanism
Analytics backends store events, not days. When you ask for a daily series, most of them run something close to:
SELECT date_trunc('day', created_at) AS x, count(*) AS y
FROM events
WHERE created_at BETWEEN $1 AND $2
GROUP BY 1 ORDER BY 1
A GROUP BY can only return groups that have rows in them. A day with no
traffic produces no row, so it never appears in the response. The API is
correct — it told you about every event it had. It just has no way to tell you
about the days it had nothing for, because those days don’t exist as data.
This bites in every charting stack I’ve used, with every analytics product
I’ve pointed at it. Umami, Plausible, Matomo, GA’s reporting API, and every
hand-rolled GROUP BY date endpoint behave the same way.
Why it renders as a lie rather than a gap
If your chart maps the array by index — bar i at position i, which is what
every quick implementation does — then removing a day doesn’t leave a hole. It
shifts everything after it left. The x-axis labels come from the same
array, so they shift too, and the chart stays internally consistent while
describing a week that never happened.
Nothing throws. Nothing looks empty. A reader sees five days of traffic and a flat-looking trend, when the truth was two days of silence followed by a spike.
It’s worse on longer ranges. Over ninety days, a site that only started getting traffic in the last fortnight returns about a dozen rows. Plotted raw, those twelve busy days spread evenly across the full width and the chart claims three months of steady traffic.
The fix: expand the window, don’t trust the response
Generate every bucket the window should contain, then merge the counts in. Server-side is the right place — do it once and the table view, the CSV export and the chart all agree.
def fill_buckets(rows, start, end, unit, tz):
got = {str(r["x"])[:10]: r["y"] or 0 for r in rows}
out, day = [], start.date()
while day <= end.date():
key = day.strftime("%Y-%m-%d")
out.append({"x": key, "y": got.get(key, 0)})
day += timedelta(days=1)
return out
Three details that matter more than they look:
- Anchor to the requested window, not to the first row you got back. If you
start the loop at
rows[0], a range whose first three days were quiet still starts late — you’ve fixed the middle and kept the bug at the edges. - Step over calendar dates, not 24-hour deltas. Adding
timedelta(days=1)to a timezone-aware datetime is absolute arithmetic. Across a DST boundary it drifts an hour, and you get a duplicated or skipped bucket once or twice a year — a bug that only reproduces in March and November. - Bucket in the reader’s timezone, not the server’s. Ask the browser
(
Intl.DateTimeFormat().resolvedOptions().timeZone) and pass it through. A “day” that ends at 5pm local reads as broken data to the person looking at it.
How to tell if you already have this bug
Count the buckets. For a seven-day daily range you should have seven or eight points, every time, regardless of traffic. If the length of the array changes with how busy the site was, your chart is editing history.
The tell in production is subtler: the first and last labels on the axis don’t match the range you asked for. If the picker says “Last 90 days” and the axis starts three weeks ago, you’re not looking at ninety days.
I’m Chris Moore — an independent developer and consultant in Henderson, NV, working under Grudged LLC. I build dashboards that answer your question rather than a vendor’s. If a chart of yours is quietly telling the wrong story, get in touch.