#323 · Dense durations: near-identical spans share a band, rows stop at what fits, the rest are listed
- timeline-app
- pull request
- sharing
Closes #316. Closes #308. ## In the editor - **Near-identical spans share one band.** Durations whose start and end are both closer than the grouping distance (**Group within**) to a band's are drawn as that band, "*Title* + 2,340 more", outlined twice. Choosing it lists them. Grouping compares distances in time, so it changes with zoom but never with panning. - **Rows go longest first,** then by start, title and ID, and still never overlap (#269). The order depends only on the durations, so panning never reshuffles the rows; a unit test and the browser test check this. - **At most what fits.** Rows that fit beneath a centered axis: at least 3, at most 8 on a phone or 16 elsewhere. The rest, and any durations the window holds beyond the 256 sent, sit behind one **+1,234 more durations ▾** chip on the next row. - **The list** (`#duration-list`): a grouped band lists its spans; the chip lists every duration touching the view. It pages 25 at a time, filters by words in titles, and sorts longest first, by start or by title. Each entry opens the duration or **Fit in view**. It's keyboard reachable, and its status is announced. In a comparison the chip lists the bands sent, and a notice gives the total. ## In the stores - **Every store sends a dense window's 256 longest and counts the rest** (`durationsTotal`, up to 10,000; `durationsTotalMore` beyond), so an era starting after thousands of short spans is still sent. Before, it was the first 256 by start. - **PostgreSQL and SQLite:** the 256 longest among the window's first 10,001 matches by start, counted in the same walk. A dense window costs at most 10,001 matches. - **Browser (`durationWindow`):** the exact 256 longest, found best-first with a priority queue of subtrees ranked by the best duration each could hold. Subtrees whose durations all match are counted from cached sizes. Nodes now cache their parsed bounds, `minLast` and `minId`, and comparisons reuse cached numerators and denominators (`Rational.compare` re-parses). A window of 5,018 durations takes about 3 ms, against 13 ms for the old first-256 walk. - **New list query:** `{"kind": "durations", "overlapping": true, lower, upper, threshold, text, sort: extent|start|title, offset, limit}` → `{durations, next (offset), total, totalMore?}`. It's on the HTTP API (validated), in OpenAPI, in the desktop native store and in memory. ## #308 The flaky "Fit in view" step was a real bug. Playwright scrolls the band into view before right-clicking, and that scroll's event arrives a frame after the menu opens, closing it. A person scrolling and right-clicking quickly would hit the same thing. The menu now remembers the page's scroll position when it opens and ignores a page scroll that ended before then. Scrolls of other elements still close it. Chromium's full `test/browser.mjs` now passes; it failed at that step every time locally. ## Tests - **Unit tests** (`test/durations.test.mjs`): - the capped count and "10,000+"; - grouping, ranking and the row cap; - layout independent of input order and of panning; - **randomized:** 40 trees, windows, thresholds and limits, each compared with brute force. - **Native:** `dense_windows_keep_their_longest_durations_count_them_and_list_them` covers the frame, count and list (search, sort, paging, invalid sort). All 26 native tests pass; clippy and fmt are clean. - **PostgreSQL** (`test/durations-postgres.mjs`): the 300-span window's total, and the same 256 as the browser index. A 3,018-duration timeline keeps all 18 longer staggered spans; the list's totals, paging, start/title sort and search are checked. - Also passing against my own database: summaries, summaries-equivalence, fork-read-through, index-versions, incremental, relationships, views, shared-history, openapi, postgres, and `test:platform:postgres`. - **Browser** (`test/dense-durations-browser.mjs`, in `test/browser.mjs`): 3,019 durations; bounded band elements; the grouped band's text; shown plus hidden equal the total; the arra…