@@ -85,6 +85,16 @@ func (m Model) updateStats(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
8585 m .statsRange = "all"
8686 m .statsLoading = true
8787 return m , loadStatsCmd ("all" )
88+ case "f" :
89+ switch m .statsBreakdown {
90+ case "" , "both" :
91+ m .statsBreakdown = "project"
92+ case "project" :
93+ m .statsBreakdown = "model"
94+ case "model" :
95+ m .statsBreakdown = "both"
96+ }
97+ return m , nil
8898 case "r" :
8999 m .statsLoading = true
90100 return m , func () tea.Msg {
@@ -108,7 +118,7 @@ func (m Model) updateStats(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
108118}
109119
110120// renderStatsView renders the full stats panel.
111- func renderStatsView (daily []stats.DailyStat , records []stats.SessionRecord , timeRange string , loading bool , width , height int ) string {
121+ func renderStatsView (daily []stats.DailyStat , records []stats.SessionRecord , timeRange , breakdown string , loading bool , width , height int ) string {
112122 if loading {
113123 return lipgloss .NewStyle ().
114124 Width (width ).
@@ -117,6 +127,10 @@ func renderStatsView(daily []stats.DailyStat, records []stats.SessionRecord, tim
117127 Render (statsDimStyle .Render ("Loading stats..." ))
118128 }
119129
130+ if breakdown == "" {
131+ breakdown = "both"
132+ }
133+
120134 var b strings.Builder
121135
122136 // Title with time range
@@ -149,30 +163,31 @@ func renderStatsView(daily []stats.DailyStat, records []stats.SessionRecord, tim
149163 b .WriteString (statsDimStyle .Render (tokens ))
150164 b .WriteString ("\n \n " )
151165
166+ barWidth := min (width - 40 , 30 )
167+ if barWidth < 10 {
168+ barWidth = 10
169+ }
170+
152171 // By Project breakdown
153- projectCosts := aggregateByProject ( daily )
154- if len ( projectCosts ) > 0 {
155- b . WriteString ( statsHeaderStyle . Render ( " By Project:" ))
156- b .WriteString (" \n " )
157- barWidth := min ( width - 40 , 30 )
158- if barWidth < 10 {
159- barWidth = 10
172+ if breakdown == "both" || breakdown == "project" {
173+ projectCosts := aggregateByProject ( daily )
174+ if len ( projectCosts ) > 0 {
175+ b .WriteString (statsHeaderStyle . Render ( " By Project:" ) )
176+ b . WriteString ( " \n " )
177+ renderBreakdown ( & b , projectCosts , totalCost , barWidth , 8 )
178+ b . WriteString ( " \n " )
160179 }
161- renderBreakdown (& b , projectCosts , totalCost , barWidth , 8 )
162- b .WriteString ("\n " )
163180 }
164181
165182 // By Model breakdown
166- modelCosts := aggregateByModel ( daily )
167- if len ( modelCosts ) > 0 {
168- b . WriteString ( statsHeaderStyle . Render ( " By Model:" ))
169- b .WriteString (" \n " )
170- barWidth := min ( width - 40 , 30 )
171- if barWidth < 10 {
172- barWidth = 10
183+ if breakdown == "both" || breakdown == "model" {
184+ modelCosts := aggregateByModel ( daily )
185+ if len ( modelCosts ) > 0 {
186+ b .WriteString (statsHeaderStyle . Render ( " By Model:" ) )
187+ b . WriteString ( " \n " )
188+ renderBreakdown ( & b , modelCosts , totalCost , barWidth , 8 )
189+ b . WriteString ( " \n " )
173190 }
174- renderBreakdown (& b , modelCosts , totalCost , barWidth , 8 )
175- b .WriteString ("\n " )
176191 }
177192
178193 // Daily trend chart
@@ -182,6 +197,12 @@ func renderStatsView(daily []stats.DailyStat, records []stats.SessionRecord, tim
182197 renderDailyChart (& b , daily , width - 6 , 6 )
183198 }
184199
200+ // Help footer
201+ b .WriteString ("\n " )
202+ breakdownLabel := map [string ]string {"both" : "all" , "project" : "project" , "model" : "model" }[breakdown ]
203+ help := fmt .Sprintf (" w:week m:month a:all f:group(%s) r:refresh" , breakdownLabel )
204+ b .WriteString (statsDimStyle .Render (help ))
205+
185206 return b .String ()
186207}
187208
@@ -295,18 +316,30 @@ func renderDailyChart(b *strings.Builder, daily []stats.DailyStat, width, maxHei
295316 }
296317
297318 // Calculate bar width
298- barSpacing := 2 // each bar + gap
299- maxBars := (width - 4 ) / barSpacing
319+ yLabelWidth := len (formatCost (maxCost )) + 1 // e.g. "$12.34 "
320+ barSpacing := 2 // each bar + gap
321+ maxBars := (width - yLabelWidth - 2 ) / barSpacing
300322 startIdx := 0
301323 if len (daily ) > maxBars {
302324 startIdx = len (daily ) - maxBars
303325 }
304326 visibleDays := daily [startIdx :]
305327
306- // Build vertical bars (top to bottom)
328+ // Build vertical bars (top to bottom) with Y-axis labels
307329 for row := maxHeight ; row >= 1 ; row -- {
308330 threshold := float64 (row ) / float64 (maxHeight )
309- line := " "
331+
332+ // Y-axis label: show at top, middle, bottom
333+ label := strings .Repeat (" " , yLabelWidth )
334+ if row == maxHeight {
335+ label = fmt .Sprintf ("%*s " , yLabelWidth - 1 , formatCost (maxCost ))
336+ } else if row == maxHeight / 2 {
337+ label = fmt .Sprintf ("%*s " , yLabelWidth - 1 , formatCost (maxCost / 2 ))
338+ } else if row == 1 {
339+ label = fmt .Sprintf ("%*s " , yLabelWidth - 1 , "$0" )
340+ }
341+ line := statsDimStyle .Render (label )
342+
310343 for _ , d := range visibleDays {
311344 ratio := d .TotalCost / maxCost
312345 if ratio >= threshold {
@@ -319,20 +352,17 @@ func renderDailyChart(b *strings.Builder, daily []stats.DailyStat, width, maxHei
319352 b .WriteString ("\n " )
320353 }
321354
322- // Date labels (just show first and last)
355+ // Date labels (first, middle, last)
323356 if len (visibleDays ) > 0 {
324- first := visibleDays [0 ].Date
325- last := visibleDays [len (visibleDays )- 1 ].Date
326- // Parse and format short
327- firstShort := shortDate (first )
328- lastShort := shortDate (last )
357+ first := shortDate (visibleDays [0 ].Date )
358+ last := shortDate (visibleDays [len (visibleDays )- 1 ].Date )
329359
330360 labelWidth := len (visibleDays ) * barSpacing
331- gap := labelWidth - len (firstShort ) - len (lastShort )
361+ gap := labelWidth - len (first ) - len (last )
332362 if gap < 1 {
333363 gap = 1
334364 }
335- b .WriteString (" " + statsDimStyle .Render (firstShort + strings .Repeat (" " , gap )+ lastShort ))
365+ b .WriteString (strings . Repeat ( " " , yLabelWidth ) + statsDimStyle .Render (first + strings .Repeat (" " , gap )+ last ))
336366 b .WriteString ("\n " )
337367 }
338368}
0 commit comments