From 8d2096767f097fb79a11a0a8362195a44c7d2727 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 12 Jul 2026 22:50:23 -0400 Subject: [PATCH] Day charts: notch only between days, first bar flush with the axis The braille glyphs already carry an intra-cell left margin, so the old leading GAP on the oldest day read as double padding against the axis border while the right edge showed single. Boundaries now pay the n-1 notch dots and the bars split the rest evenly, flush at both borders. Co-Authored-By: Claude Fable 5 --- meshterm/ui/timemachine_screen.py | 54 ++++++++++++------------ tests/test_timemachine.py | 68 +++++++++++++++++++------------ 2 files changed, 70 insertions(+), 52 deletions(-) diff --git a/meshterm/ui/timemachine_screen.py b/meshterm/ui/timemachine_screen.py index 148e397..63bc6b8 100644 --- a/meshterm/ui/timemachine_screen.py +++ b/meshterm/ui/timemachine_screen.py @@ -315,26 +315,37 @@ def _node_sections( def _day_spans(days: int, chars: int) -> list[tuple[int, int]]: - """Each day's ``(start, width)`` share of the chart's ``2 × chars`` dot columns. + """Each day's lit-bar ``(start, width)`` within the chart's ``2 × chars`` dots. The one even split behind both the bars (:func:`_day_columns`) and their axis - ticks (:func:`_day_centers`): day ``i`` runs from dot ``⌊i·D/n⌋`` to - ``⌊(i+1)·D/n⌋`` (``D = 2 × chars``), so no two days differ by more than one - dot column and the wider days interleave evenly among the narrower ones - instead of pooling at either end. The widths always sum to exactly ``D``, - keeping the bars flush with the axis border and caption sized for them. + ticks (:func:`_day_centers`). When every bar can keep at least two dots, each + of the ``n − 1`` day *boundaries* pays one unclaimed notch dot and the bars + split the rest with a floor-edge walk, so no two differ by more than one dot + column and the wider days interleave evenly among the narrower ones instead + of pooling at either end. The notches sit strictly between days: the first + bar starts flush at dot 0 (the axis border is separation enough — a leading + blank dot would double the left margin the braille glyphs already carry) + and the last ends flush at the right border, so both edges read alike. A + denser history drops the notches and the bars simply tile all ``2 × chars`` + dots. Args: days: How many day bars the chart draws. chars: The chart's width in character cells. Returns: - One ``(start dot, width in dots)`` per day, oldest first. + One ``(start dot, width in dots)`` per lit bar, oldest first. """ dots = chars * 2 n = max(1, days) - edges = [i * dots // n for i in range(n + 1)] - return [(edges[i], edges[i + 1] - edges[i]) for i in range(n)] + lit = dots - (n - 1) # what the bars keep once each boundary pays its notch dot + notch = n > 1 and lit // n >= 2 + if not notch: + lit = dots + edges = [i * lit // n for i in range(n + 1)] + return [ + (edges[i] + (i if notch else 0), edges[i + 1] - edges[i]) for i in range(n) + ] def _day_centers(days: int, chars: int) -> list[int]: @@ -460,13 +471,12 @@ def _day_columns(values: list[int], chars: int) -> list: leaving the bars short of the axis border and caption sized for the full width — misreading as the whole chart sitting shifted left). - When every day is at least three dots wide, each opens with a - :data:`~meshterm.ui.braillechart.GAP` dot column — its first dot, blank clean - down to the axis — so same-height neighbours read as separate bars instead of - fusing into one solid block. The oldest day keeps its notch too: it doubles as - the bar's one-dot clearance off the axis border, paid for inside the day's own - span. A deeper history (days of one or two dots) drops the notch chart-wide — - such a bar has no dot to spare — and the days simply abut. + The dots the split leaves unclaimed — one per day *boundary*, when the bars + are wide enough to afford them (see :func:`_day_spans`) — render as + :data:`~meshterm.ui.braillechart.GAP` columns, blank clean down to the axis, + so same-height neighbours read as separate bars instead of fusing into one + solid block. A deeper history (bars of one dot) has no boundary to spare and + the days simply abut. Args: values: Per-day counts, oldest first. @@ -476,15 +486,9 @@ def _day_columns(values: list[int], chars: int) -> list: Exactly ``2 × chars`` dot-column readings, oldest first (a :data:`~meshterm.ui.braillechart.GAP` marks a day-boundary notch). """ - spans = _day_spans(len(values), chars) - notch = min(width for _start, width in spans) >= 3 - out: list = [] - for value, (_start, width) in zip(values, spans): - if notch: - out.append(GAP) - out.extend([value] * (width - 1)) - else: - out.extend([value] * width) + out: list = [GAP] * (chars * 2) + for value, (start, width) in zip(values, _day_spans(len(values), chars)): + out[start : start + width] = [value] * width return out diff --git a/tests/test_timemachine.py b/tests/test_timemachine.py index e693b1c..4b1a359 100644 --- a/tests/test_timemachine.py +++ b/tests/test_timemachine.py @@ -249,19 +249,22 @@ def test_day_ticks_prioritise_the_newest_then_the_oldest_day() -> None: def test_day_columns_widths_differ_by_at_most_one_dot_and_interleave() -> None: """An uneven split spreads the wider days through the chart, not to one side. - 13 days over 30 chars (60 dots) can't divide evenly: days get 4 or 5 dots. - The widths must never differ by more than one dot column, and the wide days - must mix with the narrow ones instead of pooling at either end (the old - char-unit split put every wide day on the left, every narrow one on the right). + 13 days over 30 chars (60 dots) pay 12 boundary notches, leaving 48 dots of + bar that can't divide evenly: bars get 3 or 4 dots. The widths must never + differ by more than one dot column, and the wide days must mix with the + narrow ones instead of pooling at either end (the old char-unit split put + every wide day on the left, every narrow one on the right). """ cols = _day_columns([1] * 13, 30) assert len(cols) == 60 - starts = [i for i, c in enumerate(cols) if c is GAP] # one notch opens each day - widths = [b - a for a, b in zip(starts, [*starts[1:], len(cols)])] - assert len(widths) == 13 and set(widths) == {4, 5} - first_wide = widths.index(5) - last_wide = len(widths) - 1 - widths[::-1].index(5) - assert any(w == 4 for w in widths[first_wide:last_wide]) # narrow days sit between wide ones + notches = [i for i, c in enumerate(cols) if c is GAP] # one per day boundary + assert len(notches) == 12 + edges = [-1, *notches, len(cols)] # each bar runs between two notches + widths = [b - a - 1 for a, b in zip(edges, edges[1:])] + assert len(widths) == 13 and set(widths) == {3, 4} + first_wide = widths.index(4) + last_wide = len(widths) - 1 - widths[::-1].index(4) + assert any(w == 3 for w in widths[first_wide:last_wide]) # narrow days sit between wide ones def test_day_ticks_pack_the_axis_keeping_both_ends() -> None: @@ -286,13 +289,20 @@ def test_day_ticks_pack_the_axis_keeping_both_ends() -> None: assert all(b_start >= a_end + 2 for (_, a_end), (b_start, _) in zip(spans, spans[1:])) -def test_day_columns_notches_the_first_dot_of_a_multi_character_day() -> None: - """A day wide enough to span several characters opens with a GAP dot column.""" - cols = _day_columns([2, 8, 4], 12) # 3 days, 12 chars -> 4 chars (8 dots) each - assert cols[0] is GAP and cols[8] is GAP and cols[16] is GAP - assert cols[1:8] == [2] * 7 - assert cols[9:16] == [8] * 7 - assert cols[17:24] == [4] * 7 +def test_day_columns_notch_the_boundaries_and_keep_the_first_bar_flush() -> None: + """Notches sit strictly between days; the first bar starts at the axis itself. + + The braille glyphs already carry a left margin, so a leading GAP dot would + read as double padding against the axis border while the right edge (bar + flush against the closing border) shows single — the edges must match. + """ + cols = _day_columns([2, 8, 4], 12) # 24 dots − 2 notches = 22 of bar: 7+7+8 + assert cols[0] == 2 # flush against the axis — no leading notch + assert cols[:7] == [2] * 7 + assert cols[7] is GAP + assert cols[8:15] == [8] * 7 + assert cols[15] is GAP + assert cols[16:] == [4] * 8 # …and flush against the right border def test_day_columns_skips_the_notch_for_a_single_character_day() -> None: @@ -312,20 +322,24 @@ def test_day_columns_skips_notches_when_days_outnumber_characters() -> None: def test_day_chart_notch_reaches_the_axis_but_a_zero_day_keeps_its_baseline() -> None: """The notch column is blank down to the axis; a zero-value day stays a grey line. - Tall day, empty day, tall day at 4 chars each: the leading cell of each tall day - shows its right dot column only (the notch's left column blank all the way down), - and the empty day draws the faint zero baseline across its own columns. + Tall day, empty day, tall day: the boundary notches (dots 7 and 15) blank + their whole dot column so the gap runs clean to the axis border, the first + bar's very first dot is lit (flush against the axis), and the empty day + draws the faint zero baseline across its own columns. """ from meshterm.ui.braillechart import timeline_rows - cols = _day_columns([8, 0, 8], 12) # -> 4 chars (8 dot cols) per day + cols = _day_columns([8, 0, 8], 12) # 22 dots of bar (7+7+8), notches at 7 and 15 bottom = timeline_rows(cols, rows=2)[-1] - # Leading cell of the first tall day: right column filled, left (notch) blank — - # no bottom-left dot, so the gap runs clean to the axis border below. - assert not (ord(bottom.plain[0]) & 0x40) # the notch's bottom-left dot is unlit - # The empty middle day (cells 4..7) draws the faint zero baseline, in grey. - zero_cells = bottom.plain[4:8] - assert all(ord(c) & 0x80 for c in zero_cells) # each keeps its right baseline dot + assert ord(bottom.plain[0]) & 0x40 # first bar flush: its bottom-left dot is lit + # Cell 3 holds the first boundary: bar dot on its left, the notch blank on its + # right — no bottom-right dot, so the gap runs clean to the axis border below. + assert not (ord(bottom.plain[3]) & 0x80) + # The empty middle day (dots 8..14) draws the faint zero baseline, in grey. + zero_cells = bottom.plain[4:7] + assert all(ord(c) & (0x40 | 0x80) for c in zero_cells) # baseline dots present + assert ord(bottom.plain[7]) & 0x40 # the day's last baseline dot… + assert not (ord(bottom.plain[7]) & 0x80) # …then the second notch, blank zero_spans = [s for s in bottom.spans if 4 <= s.start < 8] assert zero_spans and all(s.style == "faint" for s in zero_spans)