feat(app): Spend chart matches Overview + caps to 15 days

Drop the 11px stacked-segment cap so the bars fill their columns like the
Overview .chart; render only the last 15 days.

typecheck clean; 126/126 tests pass.
This commit is contained in:
iamtoruk 2026-07-12 15:54:44 -07:00
parent 0bab74c18d
commit 9d25fc3866
2 changed files with 8 additions and 6 deletions

View file

@ -5,12 +5,13 @@ import type { DailyHistoryEntry } from '../lib/types'
const SERIES_ORDER: readonly SeriesKey[] = ['opus', 'fable', 'haiku', 'gpt', 'sonnet', 'other']
export function StackedBars({ daily }: { daily: DailyHistoryEntry[] }) {
const visibleDaily = daily.slice(-15)
const presentSeries = new Set<SeriesKey>()
const maxTotal = Math.max(
1,
...daily.map(day => day.topModels.reduce((sum, model) => sum + Math.max(0, model.cost), 0)),
...visibleDaily.map(day => day.topModels.reduce((sum, model) => sum + Math.max(0, model.cost), 0)),
)
for (const day of daily) {
for (const day of visibleDaily) {
for (const model of day.topModels) {
if (model.cost > 0) presentSeries.add(seriesKeyForModel(model.name))
}
@ -20,7 +21,7 @@ export function StackedBars({ daily }: { daily: DailyHistoryEntry[] }) {
return (
<div className="sbars-wrap">
<div className="sbars" aria-label="Daily spend by model">
{daily.map(day => (
{visibleDaily.map(day => (
<div className="c" key={day.date} data-date={day.date} title={`${day.date} · ${formatUsd(day.cost)}`}>
{[...day.topModels].sort(
(a, b) => SERIES_ORDER.indexOf(seriesKeyForModel(a.name)) - SERIES_ORDER.indexOf(seriesKeyForModel(b.name)),

View file

@ -117,9 +117,10 @@ h2 { font-size: 20px; font-weight: 650; letter-spacing: -.015em; margin: 0 0 6px
.spend-chart-panel { display: flex; flex-direction: column; }
.spend-chart-panel .pbody { display: flex; flex: 1; min-height: 0; flex-direction: column; }
.sbars-wrap { display: flex; flex: 1; min-height: 0; flex-direction: column; height: 100%; }
.sbars { display: flex; flex: 1; align-items: flex-end; gap: 3px; min-height: 120px; padding: 0 4px; }
.sbars .c { flex: 1; display: flex; flex-direction: column-reverse; height: 100%; justify-content: flex-start; align-items: center; gap: 1.5px; }
.sbars .s { width: 100%; max-width: 11px; border-radius: 0; }
.sbars { position: relative; display: flex; flex: 0 0 150px; align-items: flex-end; gap: 4px; height: 150px; padding-top: 8px; border-bottom: 1px solid var(--line2); }
.sbars::before { content: ''; position: absolute; right: 0; bottom: 36px; left: 0; height: 1px; background: var(--line2); box-shadow: 0 -37px var(--line2), 0 -74px var(--line2); pointer-events: none; }
.sbars .c { flex: 1; display: flex; flex-direction: column-reverse; height: 100%; justify-content: flex-start; gap: 1.5px; }
.sbars .s { width: 100%; border-radius: 0; }
.sbars .s:first-child { border-radius: 0 0 2px 2px; }
.sbars .s:last-child { border-radius: 2px 2px 0 0; }
.sbars .s:only-child { border-radius: 2px; }