Day charts: even dot-split columns, packed right-first ticks, honest y-marks
Day bars now share the chart's dot columns via one floor-edge split (_day_spans), so widths never differ by more than one dot and the wider days interleave with the narrower ones instead of pooling on the left. Every day at least three dots wide opens with its notch — the oldest day's doubling as its clearance off the axis border. Axis ticks pack as dense as two blank cells between labels allow, claimed newest day first, oldest second, then right to left, so a crowded axis thins from the middle-left instead of down to an evenly spaced handful. Months are written on the calendar anchors (the oldest day and the first of a month) rather than tracked across placed ticks. The y-gutter's ┤ tick crosses a braille row at its third dot, so each mark now quotes the value of a bar peaking at that dot — computed with timeline_rows' own baseline math — instead of the row's top edge, which sat a dot above where the glyph points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff76f41736
commit
7f1d8e7d10
5 files changed
+186
-100
No files matched your search
@@ -326,20 +326,31 @@ def axis_caption(
|
||||
|
||||
|
||||
def y_axis_labels(peak: float, rows: int, *, lo: float = 0.0) -> list[str]:
|
||||
"""Each chart row's top-edge value, top row first, dupes and zeros blanked.
|
||||
"""Each chart row's ticked-dot value, top row first, dupes and zeros blanked.
|
||||
|
||||
The top row always reads the peak; lower rows read the proportional values at
|
||||
their upper edges, but a mark that would repeat the one above (a low peak makes
|
||||
neighbouring rows round to the same value) or read zero is left blank, so the
|
||||
The gutter's ``┤`` tick crosses a braille row at the third dot up from the
|
||||
row's bottom, so each mark quotes the value of a bar peaking *at that dot* —
|
||||
the reading the tick visibly points at, not the row's top edge — using the
|
||||
same zero-folded scaling :func:`timeline_rows` draws bars with (baseline row
|
||||
and all). A mark that would repeat the one above (a low peak makes
|
||||
neighbouring dots round to the same value) or read zero is left blank, so the
|
||||
scale never shows the same number twice. A signed chart passes its floor as
|
||||
``lo`` (``< 0``): the marks then run linearly from ``peak`` at the top down
|
||||
through the grey zero line to ``lo`` at the bottom, so the gutter quotes both
|
||||
extremes (an SNR band's ``+5 … −10 dB``) instead of only the positive peak.
|
||||
``lo`` (``< 0``): marks above the grey zero line quote rise heights, marks
|
||||
below it hang depths, so the gutter shows both signs of a zero-crossing
|
||||
series (an SNR band's ``+5 … −10 dB``) instead of only the positive peak.
|
||||
"""
|
||||
total = rows * 4
|
||||
base = _baseline_row(lo, peak, total)
|
||||
labels: list[str] = []
|
||||
seen: set[int] = set()
|
||||
for i in range(rows):
|
||||
value = round(peak - (peak - lo) * i / rows)
|
||||
dot = (rows - 1 - i) * 4 + 2 # the dot row this row's ┤ tick crosses
|
||||
if dot > base:
|
||||
value = round(peak * (dot - base + 1) / (total - base))
|
||||
elif dot < base:
|
||||
value = round(lo * (base - dot + 1) / (base + 1))
|
||||
else:
|
||||
value = 0 # the tick sits on the zero baseline itself
|
||||
if peak != lo and value and value not in seen:
|
||||
labels.append(str(value))
|
||||
seen.add(value)
|
||||
@@ -425,7 +436,10 @@ def axis_chart(
|
||||
|
||||
Args:
|
||||
chart_rows: The chart's rows, as returned by :func:`timeline_rows`.
|
||||
peak: The value the top row's mark reads (the chart's tallest bar).
|
||||
peak: The chart's full-scale ceiling (its tallest bar's value); each
|
||||
row's gutter mark quotes the value at the dot its tick points at
|
||||
(see :func:`y_axis_labels`), so the peak itself sizes the gutter
|
||||
but is not necessarily printed.
|
||||
chars: The chart's width in character cells.
|
||||
label_at: Maps a position fraction to the x-axis caption at that point
|
||||
(a continuous chart); ignored when ``ticks`` is given.
|
||||
|
||||
@@ -30,7 +30,7 @@ from rich.console import Group, RenderableType
|
||||
from rich.text import Text
|
||||
|
||||
from ..core.models import utcnow
|
||||
from .braillechart import GAP, axis_chart, chart_span, timeline_rows
|
||||
from .braillechart import GAP, _TICK_GAP, axis_chart, chart_span, timeline_rows
|
||||
from .menus import fit_cells, section_heading
|
||||
from .theme import snr_style
|
||||
from .tui.render import render_lines
|
||||
@@ -314,12 +314,35 @@ def _node_sections(
|
||||
# --- the mesh page -----------------------------------------------------------------------
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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)]
|
||||
|
||||
|
||||
def _day_centers(days: int, chars: int) -> list[int]:
|
||||
"""The chart cell each day's bar is centred on, mirroring :func:`_day_columns`.
|
||||
|
||||
Replays the same even split ``_day_columns`` uses to spread ``days`` bars across
|
||||
``2 × chars`` dot columns, so a tick placed at ``centers[i]`` lands under day
|
||||
``i``'s bar rather than at an arbitrary fraction of the axis.
|
||||
Reads the same :func:`_day_spans` split ``_day_columns`` draws with, so a tick
|
||||
placed at ``centers[i]`` lands under day ``i``'s bar rather than at an
|
||||
arbitrary fraction of the axis.
|
||||
|
||||
Args:
|
||||
days: How many day bars the chart draws.
|
||||
@@ -328,25 +351,24 @@ def _day_centers(days: int, chars: int) -> list[int]:
|
||||
Returns:
|
||||
One centre cell (``0 .. chars - 1``) per day, oldest first.
|
||||
"""
|
||||
n = max(1, days)
|
||||
centers: list[int] = []
|
||||
dot = 0 # running dot-column offset, two per character cell
|
||||
base, extra = divmod(chars, n) if n <= chars else divmod(chars * 2, n)
|
||||
for i in range(n):
|
||||
span = (base + (1 if i < extra else 0)) * (2 if n <= chars else 1)
|
||||
centers.append(min(chars - 1, (dot + span // 2) // 2))
|
||||
dot += span
|
||||
return centers
|
||||
return [
|
||||
min(chars - 1, (start + width // 2) // 2)
|
||||
for start, width in _day_spans(days, chars)
|
||||
]
|
||||
|
||||
|
||||
def _day_ticks(shown: list, chars: int) -> list[tuple[int, str]]:
|
||||
"""``(cell, label)`` axis ticks under the day bars: short dates, thinned to fit.
|
||||
"""``(cell, label)`` axis ticks under the day bars: as many short dates as fit.
|
||||
|
||||
One tick per day where they all fit, else an evenly spaced subset keeping both
|
||||
ends; each label sits under its own bar (see :func:`_day_centers`). Dates are
|
||||
kept compact — the month is shown only on the first tick and whenever it rolls
|
||||
over, so most ticks read as a bare day number — and the newest bar reads
|
||||
``today`` when it is, mirroring the node page's closing ``now``.
|
||||
Ticks are claimed in priority order — the newest day first, the oldest day
|
||||
second, then the rest right to left — each label centred under its own bar
|
||||
(see :func:`_day_centers`) and keeping at least two blank cells from every
|
||||
label already placed, so the axis packs as dense as it can and thins from the
|
||||
middle-left outward when room runs out, never losing either end. Dates stay
|
||||
compact: the month is written only on the oldest day and on the first of a
|
||||
month (the calendar anchors), the rest read as bare day numbers, and the
|
||||
newest bar reads ``today`` when it is, mirroring the node page's closing
|
||||
``now``.
|
||||
|
||||
Args:
|
||||
shown: The charted days, oldest first, each ``(iso_date, ...)``.
|
||||
@@ -358,31 +380,36 @@ def _day_ticks(shown: list, chars: int) -> list[tuple[int, str]]:
|
||||
n = len(shown)
|
||||
centers = _day_centers(n, chars)
|
||||
today = utcnow().strftime("%Y-%m-%d")
|
||||
# A dated label is at most "Jul 12" (6 cells); keep a couple of cells between them.
|
||||
fit = max(2, chars // 8)
|
||||
if n <= fit:
|
||||
picks = list(range(n))
|
||||
else:
|
||||
picks = sorted({round(i * (n - 1) / (fit - 1)) for i in range(fit)})
|
||||
ticks: list[tuple[int, str]] = []
|
||||
prev_month: Optional[str] = None
|
||||
for i in picks:
|
||||
|
||||
def label_for(i: int) -> str:
|
||||
iso = shown[i][0]
|
||||
try:
|
||||
day = datetime.strptime(iso, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
ticks.append((centers[i], iso))
|
||||
continue
|
||||
month = day.strftime("%b")
|
||||
return iso
|
||||
if i == n - 1 and iso == today:
|
||||
label = "today"
|
||||
elif month != prev_month:
|
||||
label = f"{month} {day.day}"
|
||||
else:
|
||||
label = str(day.day)
|
||||
prev_month = month
|
||||
ticks.append((centers[i], label))
|
||||
return ticks
|
||||
return "today"
|
||||
if i == 0 or day.day == 1:
|
||||
return f"{day:%b} {day.day}"
|
||||
return str(day.day)
|
||||
|
||||
# Claimed [start, end) label spans — the same centring and spacing arithmetic
|
||||
# _tick_axis renders with, so everything placed here survives its backstop.
|
||||
taken: list[tuple[int, int]] = []
|
||||
|
||||
def claim(i: int) -> Optional[tuple[int, str]]:
|
||||
label = label_for(i)
|
||||
start = max(0, min(chars - len(label), centers[i] - len(label) // 2))
|
||||
end = start + len(label)
|
||||
if end > chars or any(
|
||||
start < e + _TICK_GAP and s < end + _TICK_GAP for s, e in taken
|
||||
):
|
||||
return None
|
||||
taken.append((start, end))
|
||||
return centers[i], label
|
||||
|
||||
order = [n - 1, *([0] if n > 1 else []), *range(n - 2, 0, -1)]
|
||||
return sorted(tick for i in order if (tick := claim(i)) is not None)
|
||||
|
||||
|
||||
def _fill_days(
|
||||
@@ -427,18 +454,19 @@ def _day_columns(values: list[int], chars: int) -> list:
|
||||
|
||||
One dot column per day leaves a short history as a sliver in a wide terminal —
|
||||
beneath how every other MeshTerm chart spends its width — so each day repeats
|
||||
over a share of the chart's columns instead. The share is an even split with
|
||||
the one-off remainder spread across the oldest days, so the total always lands
|
||||
on exactly ``2 × chars`` dot columns: a plain floor division drops the
|
||||
remainder instead, leaving the bars short of the axis border and caption
|
||||
sized for the full width (misreading as the whole chart sitting shifted left).
|
||||
over its :func:`_day_spans` share of the chart's dot columns instead: an even
|
||||
split whose widths never differ by more than one dot and whose total always
|
||||
lands on exactly ``2 × chars`` (a plain floor division drops the remainder,
|
||||
leaving the bars short of the axis border and caption sized for the full
|
||||
width — misreading as the whole chart sitting shifted left).
|
||||
|
||||
A day wide enough to span more than one character opens with a
|
||||
:data:`~meshterm.ui.braillechart.GAP` dot column — the left half of its leading
|
||||
braille cell, blank clean down to the axis — so same-height neighbours read as
|
||||
separate bars instead of fusing into one solid block. A history deeper than the
|
||||
chart is wide falls back to one dot column per day (no room for a full character
|
||||
each, so no notch), the dot-column total still landing exactly on ``2 × chars``.
|
||||
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.
|
||||
|
||||
Args:
|
||||
values: Per-day counts, oldest first.
|
||||
@@ -448,26 +476,15 @@ 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).
|
||||
"""
|
||||
n = max(1, len(values))
|
||||
spans = _day_spans(len(values), chars)
|
||||
notch = min(width for _start, width in spans) >= 3
|
||||
out: list = []
|
||||
if n <= chars:
|
||||
# Each day spans at least one whole character: split in character units so
|
||||
# every day's block starts on an even dot-column index (a fresh cell),
|
||||
# which is what makes "blank the leading cell's left half" well-defined.
|
||||
base, extra = divmod(chars, n)
|
||||
for i, value in enumerate(values):
|
||||
width_chars = base + (1 if i < extra else 0)
|
||||
if width_chars > 1:
|
||||
out.append(GAP)
|
||||
out.extend([value] * (width_chars * 2 - 1))
|
||||
else:
|
||||
out.extend([value] * (width_chars * 2))
|
||||
else:
|
||||
# More days than characters: no day gets a whole one, so no notch is drawn
|
||||
# (nothing to space apart) — just split the dot columns themselves.
|
||||
base, extra = divmod(chars * 2, n)
|
||||
for i, value in enumerate(values):
|
||||
out.extend([value] * (base + (1 if i < extra else 0)))
|
||||
for value, (_start, width) in zip(values, spans):
|
||||
if notch:
|
||||
out.append(GAP)
|
||||
out.extend([value] * (width - 1))
|
||||
else:
|
||||
out.extend([value] * width)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
+19
-13
@@ -195,9 +195,14 @@ def test_y_axis_labels_blanks_a_silent_chart() -> None:
|
||||
assert y_axis_labels(0, 3) == ["", "", ""]
|
||||
|
||||
|
||||
def test_y_axis_labels_top_row_reads_the_peak() -> None:
|
||||
"""The top row always reads the peak; lower rows are proportional."""
|
||||
assert y_axis_labels(10, 2) == ["10", "5"]
|
||||
def test_y_axis_labels_marks_quote_the_ticked_dot() -> None:
|
||||
"""Each mark reads the value at its row's third dot — where the ┤ tick points.
|
||||
|
||||
peak 10 over 2 rows (8 dots): the top row's tick crosses dot 6 (a 7-dot bar,
|
||||
10·7/8 → 9), the bottom row's dot 2 (a 3-dot bar, 10·3/8 → 4) — not the rows'
|
||||
top-edge values (10 and 5), which sit a dot above where the glyph points.
|
||||
"""
|
||||
assert y_axis_labels(10, 2) == ["9", "4"]
|
||||
|
||||
|
||||
def test_y_axis_labels_blanks_a_repeated_mark() -> None:
|
||||
@@ -210,25 +215,26 @@ def test_axis_chart_mirrors_marks_on_both_gutters() -> None:
|
||||
rows = timeline_rows([9] * 8, rows=2)
|
||||
out = axis_chart(rows, 9, 4, lambda f: "now" if f >= 1.0 else "old")
|
||||
assert len(out) == 4 # 2 chart rows + bottom border + caption
|
||||
assert out[0].plain == "9 ┤⣿⣿⣿⣿├ 9"
|
||||
assert out[1].plain == "4 ┤⣿⣿⣿⣿├ 4"
|
||||
assert out[0].plain == "8 ┤⣿⣿⣿⣿├ 8" # ticked-dot values (peak 9 sizes the gutter)
|
||||
assert out[1].plain == "3 ┤⣿⣿⣿⣿├ 3"
|
||||
assert out[2].plain.startswith(" └") and out[2].plain.endswith("┘")
|
||||
|
||||
|
||||
def test_y_axis_labels_signed_span_quotes_both_extremes() -> None:
|
||||
"""A signed chart's marks run from the peak down through zero toward the floor."""
|
||||
# rows=3 over +8..-4: top edge 8, middle 4, then 0 (blanked — it is the grey line).
|
||||
assert y_axis_labels(8, 3, lo=-4) == ["8", "4", ""]
|
||||
# A wider signed span surfaces a negative mark on the bottom row.
|
||||
assert y_axis_labels(6, 3, lo=-6) == ["6", "2", "-2"]
|
||||
"""A signed chart's marks quote rise heights above zero and hang depths below."""
|
||||
# rows=3 over +8..-4 (baseline on dot 4): the top tick reads a 7-dot rise (7),
|
||||
# the middle a 3-dot rise (3), the bottom a 3-deep hang below zero (−2).
|
||||
assert y_axis_labels(8, 3, lo=-4) == ["7", "3", "-2"]
|
||||
# A symmetric span pins the middle tick onto the baseline itself: blanked zero.
|
||||
assert y_axis_labels(6, 3, lo=-6) == ["5", "", "-4"]
|
||||
|
||||
|
||||
def test_axis_chart_floor_frames_a_signed_chart_and_widens_the_gutter() -> None:
|
||||
"""A signed floor draws negative marks and sizes the gutter for the widest one."""
|
||||
rows = timeline_rows([5, -5], rows=3, span=(-5, 5))
|
||||
out = axis_chart(rows, 5, 1, lambda f: "x", floor=-5)
|
||||
assert out[0].plain.startswith(" 5 ┤") # peak on top, gutter widened to two cells
|
||||
assert out[2].plain.startswith("-2 ┤") # a negative mark near the floor
|
||||
assert out[0].plain.startswith(" 4 ┤") # gutter widened to two cells by the floor
|
||||
assert out[2].plain.startswith("-4 ┤") # a negative mark near the floor
|
||||
|
||||
|
||||
def test_axis_chart_honours_a_shared_label_width() -> None:
|
||||
@@ -239,7 +245,7 @@ def test_axis_chart_honours_a_shared_label_width() -> None:
|
||||
"""
|
||||
rows = timeline_rows([9] * 8, rows=1)
|
||||
out = axis_chart(rows, 9, 4, lambda f: "x", label_w=3)
|
||||
assert out[0].plain.startswith(" 9 ┤")
|
||||
assert out[0].plain.startswith(" 7 ┤")
|
||||
|
||||
|
||||
# --- axis_chart column ticks ----------------------------------------------------------
|
||||
|
||||
@@ -75,12 +75,16 @@ def test_dashboard_renders_all_four_sections() -> None:
|
||||
|
||||
|
||||
def test_dashboard_activity_chart_reads_newest_right_with_mirrored_scale() -> None:
|
||||
"""'now' anchors the right edge and the peak count marks both gutters."""
|
||||
"""'now' anchors the right edge and the scale marks mirror on both gutters.
|
||||
|
||||
Peak 9 over 3 rows (12 dots): the top gutter's ┤ tick crosses its row's third
|
||||
dot (an 11-dot bar → 9·11/12 ≈ 8), so the mark reads 8, not the peak itself.
|
||||
"""
|
||||
screen = _screen(histogram=[9] + [0] * (ACTIVITY_BUCKETS - 1))
|
||||
lines = _stripped(screen.render_body(80))
|
||||
top = next(line for line in lines if "┤" in line)
|
||||
assert top.strip().startswith("9 ┤")
|
||||
assert top.rstrip().endswith("├ 9")
|
||||
assert top.strip().startswith("8 ┤")
|
||||
assert top.rstrip().endswith("├ 8")
|
||||
caption = next(line for line in lines if "now" in line)
|
||||
assert caption.index("−") < caption.index("now") # oldest left, newest right
|
||||
# The lone newest-minute burst draws against the chart's right gutter, and the
|
||||
|
||||
@@ -224,21 +224,66 @@ def test_day_ticks_shorten_dates_and_align_to_bars() -> None:
|
||||
|
||||
|
||||
def test_day_ticks_reprint_the_month_on_a_rollover() -> None:
|
||||
"""The month reappears the first time a new one starts, so a boundary reads clearly."""
|
||||
"""The first of a month always carries its month name, so a boundary reads clearly."""
|
||||
days = [(iso, 1, 1) for iso in ("2026-06-29", "2026-06-30", "2026-07-01", "2026-07-02")]
|
||||
labels = [label for _, label in _day_ticks(days, 60)]
|
||||
assert labels == ["Jun 29", "30", "Jul 1", "2"]
|
||||
|
||||
|
||||
def test_day_ticks_thin_to_fit_keeping_both_ends() -> None:
|
||||
"""More days than labels fit: an evenly spaced subset is kept, first and last included."""
|
||||
def test_day_ticks_prioritise_the_newest_then_the_oldest_day() -> None:
|
||||
"""With room for almost nothing, the newest day wins, then the oldest.
|
||||
|
||||
A chart squeezed to the minimum width still labels its "now" edge first and
|
||||
its far edge second; whatever interior days fit are claimed right to left, so
|
||||
the freshest history stays the best annotated.
|
||||
"""
|
||||
days = [(f"2026-06-{d:02d}", 1, 1) for d in range(1, 31)] # 30 days
|
||||
chars = 40 # ~5 dated labels fit (chars // 8)
|
||||
chars = 20
|
||||
ticks = _day_ticks(days, chars)
|
||||
centers = _day_centers(len(days), chars)
|
||||
assert 2 <= len(ticks) <= 6
|
||||
cols = [cell for cell, _ in ticks]
|
||||
assert cols[0] == centers[0] and cols[-1] == centers[-1] # both ends survive the thinning
|
||||
assert centers[-1] in cols # the newest day always keeps its tick
|
||||
assert centers[0] in cols # …and the oldest is claimed right after it
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def test_day_ticks_pack_the_axis_keeping_both_ends() -> None:
|
||||
"""More days than labels fit: the axis packs what it can, ends always included.
|
||||
|
||||
Ticks are claimed newest-first, then oldest, then right to left, each keeping
|
||||
two blank cells from its neighbours — so a crowded axis stays dense (well past
|
||||
the old evenly-thinned handful) and any dropped days come from the interior.
|
||||
"""
|
||||
days = [(f"2026-06-{d:02d}", 1, 1) for d in range(1, 31)] # 30 days
|
||||
chars = 40
|
||||
ticks = _day_ticks(days, chars)
|
||||
centers = _day_centers(len(days), chars)
|
||||
cols = [cell for cell, _ in ticks]
|
||||
assert cols[0] == centers[0] and cols[-1] == centers[-1] # both ends survive
|
||||
assert len(ticks) >= 8 # bare day numbers pack far denser than dated labels
|
||||
# Every placed label keeps at least two blank cells from the one before it.
|
||||
spans = []
|
||||
for cell, label in ticks:
|
||||
start = max(0, min(chars - len(label), cell - len(label) // 2))
|
||||
spans.append((start, start + len(label)))
|
||||
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:
|
||||
|
||||
Reference in new issue
Block a user