Show session lengths in ten categories

The Focus view grouped sessions into six buckets, too coarse to see a
rhythm. There are now ten: under 5m, 5-15m, 15-25m, 25-45m, 45m-1h,
1h-1h30m, 1h30m-2h, 2-3h, 3-4h and 4h or more. 25 minutes is a classic
pomodoro, and the long end separates 3-4h stretches from 4h+ hyperfocus.

Ten labelled columns don't fit side by side, so the chart is now drawn
with horizontal bars: every label stays readable at any width.

The buckets are one constant (analysis::focus::SESSION_BUCKETS); a new
test pins down where sessions land, including an exact boundary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-09-25 00:25:14 -04:00
1 parent 49e14b820a
commit ae9e1b1f08
2 files changed
+52 -9

No files matched your search

+44 -3
View File
@@ -10,9 +10,21 @@ use super::{days_in, median};
use crate::domain::WorkDay; use crate::domain::WorkDay;
use crate::time::DateRange; use crate::time::DateRange;
/// Session lengths are counted in these buckets: (label, upper limit in minutes). /// Session lengths are counted in these buckets: (label, upper limit in
pub const SESSION_BUCKETS: [(&str, i64); 6] = /// minutes). 25 minutes is a classic pomodoro. Change them freely: the
[("<15m", 15), ("15-30m", 30), ("30m-1h", 60), ("1-2h", 120), ("2-3h", 180), ("3h+", i64::MAX)]; /// Insights screen follows.
pub const SESSION_BUCKETS: [(&str, i64); 10] = [
("under 5m", 5),
("5-15m", 15),
("15-25m", 25),
("25-45m", 45),
("45m-1h", 60),
("1h-1h30m", 90),
("1h30m-2h", 120),
("2-3h", 180),
("3-4h", 240),
("4h or more", i64::MAX),
];
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Focus { pub struct Focus {
@@ -73,3 +85,32 @@ pub fn focus(days: &[WorkDay], range: DateRange, zone: &TimeZone, long_session:
past_midnight, past_midnight,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{Journal, Segment, ShiftState};
use jiff::civil::date;
#[test]
fn sessions_land_in_the_right_bucket() {
// Sessions of 3m, 20m, exactly 25m, 50m and 5h, with 10m breaks.
let lengths = [3, 20, 25, 50, 300];
let mut start = date(2026, 9, 24).at(8, 0, 0, 0).to_zoned(TimeZone::UTC).unwrap().timestamp();
let mut segments = Vec::new();
for minutes in lengths {
let end = start + SignedDuration::from_mins(minutes);
segments.push(Segment::new(start, end).unwrap());
start = end + SignedDuration::from_mins(10);
}
let journal = Journal::from_parts(vec![(date(2026, 9, 24), segments)], ShiftState::Idle).unwrap();
let days = journal.snapshot(start);
let range = DateRange::last_days(date(2026, 9, 24), 7);
let result = focus(&days, range, &TimeZone::UTC, SignedDuration::from_hours(2));
// under 5m, 5-15m, 15-25m, 25-45m, 45m-1h, ..., 4h or more
assert_eq!(result.session_counts, [1, 0, 1, 1, 1, 0, 0, 0, 0, 1]);
assert_eq!(result.long_sessions, 1);
assert_eq!(result.breaks, 4);
}
}
+8 -6
View File
@@ -12,7 +12,7 @@ use jiff::civil::{Date, Weekday};
use jiff::{SignedDuration, ToSpan}; use jiff::{SignedDuration, ToSpan};
use ratatui::Frame; use ratatui::Frame;
use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Position, Rect};
use ratatui::style::{Modifier, Style}; use ratatui::style::{Modifier, Style};
use ratatui::symbols::Marker; use ratatui::symbols::Marker;
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
@@ -375,14 +375,16 @@ impl Insights {
.style(Style::new().fg(ctx.theme.working)) .style(Style::new().fg(ctx.theme.working))
}) })
.collect(); .collect();
let bar_width = (chart_area.width.saturating_sub(2) / SESSION_BUCKETS.len() as u16) // Horizontal bars, one row each, so every label fits; a blank row
.saturating_sub(2) // between bars when there's room.
.clamp(3, 12); let rows = chart_area.height.saturating_sub(2);
let gap = u16::from(rows >= 2 * SESSION_BUCKETS.len() as u16 - 1);
frame.render_widget( frame.render_widget(
BarChart::new(bars) BarChart::new(bars)
.direction(Direction::Horizontal)
.block(section(" How long your sessions last ", ctx)) .block(section(" How long your sessions last ", ctx))
.bar_width(bar_width) .bar_width(1)
.bar_gap(2) .bar_gap(gap)
.value_style(Style::new().fg(ratatui::style::Color::Black).bg(ctx.theme.working)), .value_style(Style::new().fg(ratatui::style::Color::Black).bg(ctx.theme.working)),
chart_area, chart_area,
); );