From dbd93fee51c4d155139f6113aeb6a130d1105d7d Mon Sep 17 00:00:00 2001
From: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
Date: Mon, 20 Jul 2026 08:03:24 -0700
Subject: [PATCH] app,dash: render pre-history days as no data, not a currency
zero (#765)
* fix(app,dash): render pre-history days as no data, not a currency zero
Days before the first recorded day in history.daily were painted as a real
0.00/0 calls in the daily activity heatmap and the daily spend charts. That
zero is unknown, not measured, so those cells and bars now read "No data
recorded" instead of a currency zero. Genuinely idle days within recorded
history stay as real zeros.
- app: heatmap cells, the daily spend bars, and the daily-by-model columns
get a distinct no-data style plus a "No data recorded" hover for days before
the first recorded day.
- dash: the granular line chart drops buckets before the first recorded day so
no flat zero line is drawn before any history exists.
- data-start is derived in the UI layer from history.daily; payload shape is
unchanged.
Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and
root typecheck clean; dash and app renderer vite builds succeed.
* no-data: cap guard, timezone-free dash trim, StackedBars aria
Three hardening fixes from adversarial review of the no-data rendering:
dataStartKey returns null at the payload's 365-entry history cap, where
the oldest retained entry is no longer the true data start; classifying
past it would label real aged-out history as no data on long custom
ranges. Documented that the install-to-first-use gap reading as no data
is literally accurate: nothing was recorded then either.
The dash granular chart now trims leading zero-only buckets by value
instead of comparing bucket timestamps against date keys, so producer
and viewer timezone skew can never drop a real first-day bucket, and an
all-zero series lands in the established empty state.
StackedBars no-data columns expose their state via aria-label, not only
a title on a non-focusable div.
---
.../components/ActivityHeatmap.test.tsx | 50 ++++++++
app/renderer/components/ActivityHeatmap.tsx | 33 ++++--
app/renderer/components/StackedBars.test.tsx | 28 +++++
app/renderer/components/StackedBars.tsx | 67 ++++++-----
app/renderer/lib/period.test.ts | 107 ++++--------------
app/renderer/lib/period.ts | 25 ++++
app/renderer/sections/Overview.test.tsx | 21 ++++
app/renderer/sections/Overview.tsx | 53 +++++----
app/renderer/sections/Spend.tsx | 5 +-
app/renderer/styles/indigo.css | 2 +
app/renderer/styles/plain.css | 5 +
dash/src/components/UsageChart.tsx | 12 +-
12 files changed, 262 insertions(+), 146 deletions(-)
create mode 100644 app/renderer/components/ActivityHeatmap.test.tsx
diff --git a/app/renderer/components/ActivityHeatmap.test.tsx b/app/renderer/components/ActivityHeatmap.test.tsx
new file mode 100644
index 00000000..59db5ed4
--- /dev/null
+++ b/app/renderer/components/ActivityHeatmap.test.tsx
@@ -0,0 +1,50 @@
+// @vitest-environment jsdom
+import { fireEvent, render } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import type { DailyHistoryEntry } from '../lib/types'
+import { ActivityHeatmap } from './ActivityHeatmap'
+
+function entry(date: string, cost: number, calls: number): DailyHistoryEntry {
+ return { date, cost, savingsUSD: 0, calls, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, topModels: [] }
+}
+
+// A fixed "now" so the 26-week window and the day-under-test are deterministic.
+beforeEach(() => vi.setSystemTime(new Date(2026, 6, 20, 12, 0, 0)))
+afterEach(() => vi.useRealTimers())
+
+describe('ActivityHeatmap no-data days (before recorded history)', () => {
+ // History begins 2026-07-10; earlier days predate any recorded data.
+ const daily = [entry('2026-07-10', 4, 20), entry('2026-07-15', 6, 30)]
+
+ it('marks days before the first recorded day as no data, not a currency zero', () => {
+ const { container } = render(