Repository navigation
feat: Full Timeline overview below the Timeline graph (CLUE-719) - #3029
Conversation
The Full Timeline shows the whole record as a black waveform on white, with every event drawn in its own accessible color set, and an overlay marking the part of the record shown in the graph. Dragging the overlay or the scrollbar thumb moves the view, keeping the grabbed point under the pointer; pressing elsewhere on either centers the view there. Each drag is saved, and undone, as a single change, and neither does anything in read-only views. While the thumb has keyboard focus, the overlay shows the focus ring. Each event type now has a shape as well as a color - square, circle, triangle, inverted triangle, bars, diamond, then "?" - shown under the event's number in the graph and above the event in the Full Timeline. - DynamicScrollbar gains track presses, scrub start and end callbacks, a disabled state and hover and drag styles; useScrub holds the press-and-drag logic it shares with the Full Timeline - WaveformPanel gains an "overview" mode - The graph's unit label no longer overlaps the graph Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3029 +/- ##
==========================================
+ Coverage 87.07% 87.13% +0.05%
==========================================
Files 1059 1063 +4
Lines 60837 60939 +102
Branches 16246 16272 +26
==========================================
+ Hits 52974 53097 +123
+ Misses 7839 7818 -21
Partials 24 24
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-719-full-timeline
|
| Run status |
|
| Run duration | 03m 47s |
| Commit |
|
| Committer | Kirk Swenson |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
There was a problem hiding this comment.
🟡 Changes recommended
Concurrent scrub sessions and mismatched minimum-width geometry can break gesture history and visual synchronization.
2 open findings
What changed in this PR
Adds a navigable Full Timeline overview to the Timeline tile, including event shapes, scrubbing, keyboard navigation, accessibility announcements, and waveform overview rendering.
Changes:
- Adds the Full Timeline strip and shared scrub interactions.
- Adds shape-based event identification and darker overview colors.
- Extends waveform and scrollbar components with overview and read-only support.
| File | Description |
|---|---|
src/plugins/timeline/components/timeline.tsx |
Integrates Full Timeline. |
src/plugins/timeline/components/timeline.scss |
Adjusts graph and unit-label layout. |
src/plugins/timeline/components/timeline-scrollbar.tsx |
Removes obsolete wrapper. |
src/plugins/timeline/components/timeline-plot.tsx |
Shares view descriptions. |
src/plugins/timeline/components/timeline-event-colors.scss |
Adds overview colors. |
src/plugins/timeline/components/full-timeline.tsx |
Implements overview navigation. |
src/plugins/timeline/components/full-timeline.test.tsx |
Tests overview behavior. |
src/plugins/timeline/components/full-timeline.scss |
Styles overview and overlay. |
src/plugins/timeline/components/event-shape.tsx |
Renders event shapes. |
src/plugins/timeline/components/event-shape.test.tsx |
Tests event shapes. |
src/plugins/timeline/components/event-shape.scss |
Styles event shapes. |
src/plugins/timeline/components/event-overlay.tsx |
Adds shapes to graph events. |
src/plugins/timeline/components/event-overlay.test.tsx |
Tests graph event shapes. |
src/plugins/timeline/components/event-overlay.scss |
Positions graph event shapes. |
src/plugins/timeline/components/describe-view.ts |
Shares announcement formatting. |
src/plugins/shared-seismogram/components/waveform-panel.tsx |
Adds overview rendering mode. |
src/plugins/shared-seismogram/components/waveform-panel.test.tsx |
Tests overview waveform colors. |
src/plugins/shared-seismogram/components/waveform-panel.scss |
Styles overview background. |
src/components/ui/use-scrub.ts |
Adds reusable pointer scrubbing. |
src/components/ui/dynamic-scrollbar.tsx |
Adds track scrubbing and disabling. |
src/components/ui/dynamic-scrollbar.test.tsx |
Expands scrollbar interaction tests. |
src/components/ui/dynamic-scrollbar.scss |
Adds interaction-state styling. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
- View previews nest, so overlapping gestures on the strip, the scrollbar and the graph are saved together, as one change, when the last one ends - The overlay and scrollbar thumb stay inside their track when widened to their 8px minimum at the right end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A press on the Full Timeline or the scrollbar selects the Timeline tile; the tile now listens for pointerdown, which those controls cancel - A thumb or overlay widened to its minimum width drags all the way to either end - A scrub in progress when the data range goes away ends and is saved, rather than leaving every later view change unsaved - Events outside the data range are clipped to the strip or left out, and moving the view no longer redraws the events - The scrollbar thumb reads the range in view as its value, and a press that leaves the view where it was announces nothing - The thumb rests at 45% black (55% hover, 65% drag) for 3:1 contrast with its track - WaveformPanel's mode classes are prefixed (mode-overview); the 8px minimum width and the overlay teal are shared rather than repeated - Tests for a second pointer, lost pointer capture, overlapping drags saved as one change, and tile selection; the pointer tests share one PointerEvent mock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Overlapping or sub-second scrubs can miss announcements, and boundary-only events are rendered incorrectly.
2 open findings
2 resolved since last review
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
- Overlapping scrubs of the strip and the scrollbar are announced once, when the last one ends, if the view moved at all - Events that only touch the start or end of the data are left out, as the graph's visible events are Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Previews nest, so a missing endViewPreview leaves every later view change unsaved. An end without a matching begin means the calls have gotten out of step, so warn about it in development and tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
emcelroy
left a comment
There was a problem hiding this comment.
Looks good 👍 Excerpts from a Claude-generated review are below. They include some minor issues to consider. Also, you're probably already aware, but there are conflicts to resolve before merging.
This is good work. The code does what the PR description says. The new useScrub hook is small and handles the hard cases: a canceled pointer, a lost pointer capture, a second pointer, unmount during a drag, and a thumb that is drawn wider than the view. When two drags overlap, their changes are saved together when the last drag ends. This works because beginViewPreview/endViewPreview now nest, and the unmount cleanup in useScrub and TimelinePlot keeps the count in balance. FullTimelineEvents does not read the view, so the events are not drawn again when the view moves.
FullTimelineStrip is the only user of DynamicScrollbar, so the changes to that component do not affect other code.
I found no blocking problems. Two findings are small confirmed problems, and two are design questions. The rest are nits.
Findings
1. Records shorter than 2 seconds: the scrollbar can move the view past the end of the data (low)
DynamicScrollbarsetstotalRange = max(data range, minViewRange)and givestotalStart + totalRangetouseScrubas the end of the track (src/components/ui/dynamic-scrollbar.tsx:73).FullTimelinesetsminViewRangeto 2000 ms.- Example: a record of 1 second. Zooming is limited to the data range, so the view is also 1 second.
- The scrollbar track is 2 seconds long. If you drag the thumb to the right, the view can end at
dataStart + 2s, which is after the end of the data. - The keyboard does the same thing.
shiftViewalso limits the view to the extendedtotalRange, so the End key moves the view past the end of the data. - The strip uses the real data range, so it stops at the end of the data. The two controls then do not agree, and the thumb and the overlay do not have the same width.
- The clamp in
timeline.tsxruns only when the data range changes, so it does not correct this.
The old scrollbar had the same math, so the problem is not new. But the new overlay makes the difference easy to see. Also, the PR description says "The thumb and overlay are always the same width", and that is not true in this case. This can be a follow-up.
2. disabled does not stop a drag that has already started (low, hardening)
disabled is read only in onPointerDown (src/components/ui/use-scrub.ts:57). If disabled becomes true during a drag, onPointerMove still calls onViewChange until pointerup.
In this PR, disabled is always readOnly. That value is fixed for each document pane, so I do not think this can happen now. But useScrub and DynamicScrollbar are generic, and a later caller could change disabled during a drag.
If you fix it, add the check to onPointerMove:
if (!drag || drag.pointerId !== e.pointerId || disabled) return;
- Do not add the same check to
endScrub. If you do,onScrubEndis never called, and the preview count stays above zero. Then every later view change stays unsaved. - Ending the drag in an effect when
disabledchanges is more work and has more risk. The effect must release pointer capture throughtrackRef, and it repeats most ofendScrub.
Related, on the base branch: when readOnly is true, TimelinePlot renders without pointer handlers (src/plugins/timeline/components/timeline-plot.tsx:143). If readOnly becomes true during a graph drag that has moved, endViewPreview is not called until the plot unmounts. Until then, view changes from every control stay unsaved. The trigger is equally unlikely, but if you change useScrub, it is a good time to make the graph consistent.
3. Read-only views: should there be a focusable control that states the range? (accessibility, question)
- In read-only views,
disabledsetstabIndex={-1}on the thumb (dynamic-scrollbar.tsx:111), and the strip hasaria-hidden="true"(src/plugins/timeline/components/full-timeline.tsx:102). - These users do not lose the time range. The start and end times are plain text below the graph (
TimeLabelin thetimeline-range-rowoftimeline.tsx).tabIndex={-1}does not remove the thumb from the accessibility tree, so a screen reader user can still reach the slider and itsaria-valuetextby reading through the page. - What they lose:
- a Tab stop that states the range
- the thumb's percentage, which tells where the view sits in the whole record
- W3C guidance treats focus on disabled controls as a choice about how easy a control is to find, not as a rule.
- Question: do read-only users need a focusable control that states the range? If yes, keep the thumb at
tabIndex={0}. It already hasaria-disabled, andhandleKeyDownalready ignores keys when disabled. Two tests requiretabindex="-1"and would then need to change:src/components/ui/dynamic-scrollbar.test.tsx:379and the read-only test infull-timeline.test.tsx.
4. The "Full Timeline" label can hide event shapes (visual, question)
.full-timeline-label has a white background and is centered on the top edge of the strip. It comes after the event shapes in the DOM, and the shapes are drawn at top: -9px (src/plugins/timeline/components/full-timeline.scss:58). Thus the label can cover the shape of an event near the middle of the record.
In the design spec, the label similarly covers event shapes. So it's likely that's acceptable, but I thought it was worth noting just to make sure we're all aware of that.
5. Small points (nits)
hasSelectionModifier(src/utilities/event-utils.ts:13) does not listPointerEventin its signature. The code compiles becausePointerEventextendsMouseEvent. Adding it would make the newpointerdownuse clearer.- In the Full Timeline, the
?shape is drawn at about 6.5px. It may be hard to read. The PR says the shapes were "checked by the reviewer's eye", so check this one on the branch preview.
Tests
Tests that claim more than they prove
- "is selected by a press that a control cancels" (
src/plugins/timeline/components/timeline-tile.test.tsx:172). The test adds a listener that cancelspointerdown. That listener has no effect, because jsdom never sends themousedownthat a browser sends afterpointerdown. The test proves only that apointerdownselects the tile. That still catches a return to the oldmousedownhandler. But the name suggests that the cancel case was checked, and it was not. Suggestion: rename the test, or remove the listener and add a comment. - "grabs the overlay … as at its minimum width" (
full-timeline.test.tsx:169). jsdom does not apply CSS, so in the test the overlay is never wider than the view. The test sends the press directly to the overlay at a point outside the view. It checks the correct code path, but not the CSS part. The name says that the case is simulated, so this is acceptable.
Gaps
- Undo is not tested. No test runs undo after a drag. A test that undoes once and checks that both fields go back to their old values would prove the "single change" claim in the PR description.
- Right-click is not tested.
useScrubignorese.button !== 0, but no scrollbar or Full Timeline test checks this. The graph has a test for it (timeline-plot.test.tsx:120). - The rounding in
handleViewChangeis not tested. In the tests, 1px is exactly 345,600 ms, so every value is already a whole number. To test the rounding, use a track width that gives fractions. - The keyboard is not tested through
FullTimeline. No test checks that an arrow key changes the model and saves the change right away. - The overview height (38px) is not tested. The waveform test checks only the stroke and fill colors.
- Minor: the event tests check the position of only one of the two events. The overlay test says the label is "centered on the event", but it does not compare the shape's position with the button's position.
Code comments
Almost all the comments are correct but there are some minor issues.
- Misleading:
src/plugins/timeline/models/timeline-content.ts:241, "Unpaired calls are a bug: a missing end leaves later view changes unsaved."- This comment is inside the check for an extra end, but it explains the result of a missing end. This check cannot detect a missing end.
- Here is what the check actually prevents. Without it, the count goes to -1. The next
beginViewPreviewthen does not start a preview, so that gesture saves a change on every move. - Suggested text: "An end without a begin is a bug. Ignore it, so that the count can't go below zero and stop the next preview from starting."
- Imprecise:
use-scrub.ts:126.grabOffsetis "Distance from the start of the view to the point under the pointer". This is not true for a thumb drawn wider than the view, because the code then limits the offset to the view. Add "limited to the view". - Imprecise:
dynamic-scrollbar.test.tsx:354, "the thumb is drawn at its minimum width". In jsdom the thumb is not drawn wider. Use "would be drawn". - Small point: the
describeViewdoc says the text is for screen reader announcements. It is now also used as the scrollbar'saria-valuetext.
Brings in the final review round of the Timeline zoom and pan PR. Its saved-view check (an undo mid-drag discards the preview) now applies when the outermost of any nested previews ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep the view within records shorter than 2 seconds. The scrollbar no longer stretches its track to the shortest view, so the thumb and the overlay match and the view can't move past the data's end. - A scrub that is disabled midway stops moving the view, and still ends normally. - Clarify comments on preview nesting, `grabOffset` and `describeView`, and the name of a tile selection test that jsdom can't fully check. - Accept pointer events in `hasSelectionModifier`'s signature. - Test undo after a previewed gesture, scrollbar keys through the Full Timeline, and that right-button presses are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@emcelroy Thanks for the review! I merged master, which resolved the conflicts, and addressed most of your points in e48a5f7:
The merge also combines master's new check (an undo during a drag discards the drag instead of being saved over) with this branch's nested previews. The check now runs when the last nested preview ends. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

CLUE-719
The Timeline tile now has a Full Timeline below the graph: the whole record at a glance, with a marker for the part the graph is showing. On a record spanning weeks or months, students can see where they are and get anywhere in one gesture, instead of hunting with the scrollbar alone.
Event shapes: each event type now has a shape as well as a color: square, circle, triangle, inverted triangle, bars, diamond, then "?" for any type beyond the six colors. The shape appears under the event's number in the graph and, at half size, above the event in the Full Timeline.
Fix: the graph's unit label (e.g. "M/S**2") no longer overlaps the graph.
Design note: the scrollbar thumb is 45% black at rest, 55% on hover and 65% while dragging, a step darker than Zeplin's 40/50/60%, so that it has 3:1 contrast with its track.
Implementation
useScrubhook (src/components/ui/) holds the press-and-drag logic shared by the Full Timeline and the scrollbar: grab the view if the press is on it, otherwise center it on the press; clamp at the ends; pointer capture;onScrubStart/onScrubEnd.DynamicScrollbarstays a generic component and gains track presses (viauseScrub),onScrubStart/onScrubEnd, adisabledprop that also takes the thumb out of the tab order, athumbValueTextprop foraria-valuetext, and the Zeplin hover and drag states.TimelineScrollbar, its thin wrapper, is replaced byFullTimeline, which renders the strip and the scrollbar together and maps the scrub callbacks tobeginViewPreview/endViewPreview.beginViewPreview/endViewPreviewnow nest, so overlapping gestures are saved together when the last one ends.pointerdowninstead ofmousedown/touchstart, because the Full Timeline and the scrollbar cancelpointerdown, which suppresses themousedownthat would follow.WaveformPanelgains an"overview"mode (black, 38px tall); the existing modes are unchanged. The overview is a second panel loading the full data range under its own caller id.EventShapecomponent draws the shape for an event's color.describeView(the announcement text) moves out ofTimelinePlotso the Full Timeline can share it.PointerEventmock,src/test/pointer-events.ts, instead of three copies.Not in this PR
SeismicQueryService's shared cache and predates this PR, which makes it more likely; it's filed as CLUE-722.Testing
useScrubthroughDynamicScrollbar: track press centering and clamping, grabbing within the view or on a thumb widened past it, a second pointer's moves and presses ignored, scrub start and end, cancel, lost pointer capture, unmount, disabled.FullTimeline: overlay and event placement, events clipped to the strip, centering, grabbing the overlay, saving a drag only when it ends, overlapping strip and scrollbar drags saved as one change (counted), a scrub ended and saved when the data goes away, read-only, the scrollbar's label and value text, and announcing only when the view moves.EventShape,EventOverlay, and the overview mode ofWaveformPanel.Builds on #3027 (CLUE-673); this PR targets that branch until it merges. The navigation info modal (CLUE-720) follows separately.
🤖 Generated with Claude Code