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:
1 parent
49e14b820a
commit
ae9e1b1f08
2 files changed
+52
-9
No files matched your search
+44
-3
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user