Skip to content

fix(ui): create independent play-head indicator - #528

Merged
rapids-bot[bot] merged 12 commits into
rapidsai:mainfrom
cmatzenbach:independent-play-head-indicator
Aug 5, 2026
Merged

fix(ui): create independent play-head indicator#528
rapids-bot[bot] merged 12 commits into
rapidsai:mainfrom
cmatzenbach:independent-play-head-indicator

Conversation

@cmatzenbach

Copy link
Copy Markdown
Contributor

Description

Creates a separate play-head line indicator so that it does not interfere with the current x-axis pointer that shows where the user is currently focused on the timelines.

Related Issues

Closes #487

Testing

  1. Start the data flow player
  2. Ensure a new solid line plays over the timelines for the data flow
  3. Hover over the timelines, ensuring a separate dotted line appears to show current hover state
  4. Ensure both lines function independently and do not interfere with one another

Screenshots

play-head-indicator.mp4

@cmatzenbach
cmatzenbach requested a review from johallar as a code owner August 3, 2026 19:43
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces synced-pointer playhead synchronization with shared playhead-line timestamp state, adds pixel conversion for timeline overlays, renders independent vertical playhead lines in timeline views, and updates playback tests to validate the new playhead-line notifications.

Changes

Independent playhead indicator

Layer / File(s) Summary
Playhead line state updates
ui/packages/@quent/hooks/src/atoms/dataFlow.ts, ui/packages/@quent/hooks/src/dataFlow/dataFlowSelectors.ts, ui/packages/@quent/hooks/src/index.ts, ui/packages/@quent/components/src/dag/DagPlayhead.tsx, ui/src/components/DataFlowOverlay.test.tsx
Adds playheadLineTimeMsAtom and exported selector hooks. DagPlayhead now sets and clears the playhead-line timestamp during drag, playback, disable, stop, and cleanup paths. The playback test now asserts subscribePlayheadLine notifications instead of axis-pointer actions.
Timestamp-to-pixel conversion
ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts
Adds usePlayheadLinePixel. The hook converts the current playhead-line timestamp to an ECharts x-axis pixel, recomputes on timestamp, zoom, and chart-finished events, and clears invalid or unavailable values.
Timeline overlay rendering
ui/packages/@quent/components/src/timeline/Timeline.tsx, ui/packages/@quent/components/src/timeline/TimelineController.tsx
Both timeline views now compute the playhead-line pixel from the chart instance and render a non-interactive vertical overlay line when the position is available. Timeline also tracks chart readiness to trigger recalculation after setup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • rapidsai/quent#393: It changes the same playhead synchronization path in DagPlayhead, data-flow atoms/selectors, and overlay tests.
  • rapidsai/quent#447: It also updates timeline playhead synchronization and overlay rendering in DagPlayhead.tsx, Timeline.tsx, and TimelineController.tsx.
  • rapidsai/quent#497: It modifies timeline and chart playhead synchronization around ECharts axis-pointer behavior that this PR replaces.

Suggested labels: bug

Suggested reviewers: johallar

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding an independent play-head indicator in the UI.
Description check ✅ Passed The description includes the required sections, linked issue, testing steps, and a screenshot for the UI change.
Linked Issues check ✅ Passed The changes implement a separate play-head indicator independent of the x-axis pointer, matching issue #487.
Out of Scope Changes check ✅ Passed All changes support the independent play-head indicator; no unrelated code changes are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/packages/`@quent/components/src/lib/usePlayheadLinePixel.ts:
- Around line 19-33: The usePlayheadLinePixel subscription only updates on
playhead broadcasts, leaving the overlay stale after chart mapping or size
changes. Update usePlayheadLinePixel to retain the latest timestamp and
recompute the pixel after chart readiness, dataZoom, and resize events, while
preserving null/error handling and cleaning up every added listener.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 0cb594c6-619f-4958-b792-214835a3244e

📥 Commits

Reviewing files that changed from the base of the PR and between 153d422 and d78f5fc.

📒 Files selected for processing (5)
  • ui/packages/@quent/components/src/dag/DagPlayhead.tsx
  • ui/packages/@quent/components/src/lib/timeline.utils.ts
  • ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts
  • ui/packages/@quent/components/src/timeline/Timeline.tsx
  • ui/packages/@quent/components/src/timeline/TimelineController.tsx

Comment thread ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/packages/@quent/components/src/lib/timeline.utils.ts (1)

515-535: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Replay the current playhead state for late subscribers.

If a timeline subscribes after broadcastPlayheadLine emits a timestamp and before hidePlayheadLine, usePlayheadLinePixel retains its initial null and hides the overlay until the next broadcast. Store the latest number | null value and deliver it during subscription.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/packages/`@quent/components/src/lib/timeline.utils.ts around lines 515 -
535, Update subscribePlayheadLine and the shared playhead state so the latest
number or null value is stored whenever broadcastPlayheadLine or
hidePlayheadLine runs, then immediately invoke each newly subscribed listener
with that stored value. Preserve existing listener registration and
unsubscription behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ui/packages/`@quent/components/src/lib/timeline.utils.ts:
- Around line 515-535: Update subscribePlayheadLine and the shared playhead
state so the latest number or null value is stored whenever
broadcastPlayheadLine or hidePlayheadLine runs, then immediately invoke each
newly subscribed listener with that stored value. Preserve existing listener
registration and unsubscription behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: ad74c9f2-2cd7-45a6-937c-1ebb833e320a

📥 Commits

Reviewing files that changed from the base of the PR and between d78f5fc and 1a5d9f5.

📒 Files selected for processing (4)
  • ui/packages/@quent/components/src/lib/timeline.utils.ts
  • ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts
  • ui/packages/@quent/components/src/timeline/Timeline.tsx
  • ui/packages/@quent/components/src/timeline/TimelineController.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ui/packages/@quent/components/src/lib/timeline.utils.ts (1)

529-535: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Broadcast every playhead state update.

usePlayheadLinePixel.ts updates only from subscribePlayheadLine. The handleKeyDown and restart paths in DagPlayhead.tsx update playheadTimeS without calling broadcastPlayheadLine. The chart line can remain stale or hidden after keyboard input. Broadcast each state change or centralize the state update and broadcast in one helper.

Based on the downstream DagPlayhead.tsx and usePlayheadLinePixel.ts paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/packages/`@quent/components/src/lib/timeline.utils.ts around lines 529 -
535, Ensure every playheadTimeS state update, including keyboard handling and
restart paths in DagPlayhead, broadcasts the new value through
broadcastPlayheadLine so usePlayheadLinePixel stays synchronized. Prefer
centralizing the state update and broadcast in one helper, and preserve
hidePlayheadLine behavior when the playhead is cleared.
ui/packages/@quent/components/src/index.ts (1)

167-171: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restore the package-root Timeline export.

Timeline remains a named component and supported examples import it from @quent/components. Removing the export breaks those consumers. Keep the export or provide an intentional migration and release note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/packages/`@quent/components/src/index.ts around lines 167 - 171, Restore
the package-root named Timeline export in the index.ts export list, using the
existing Timeline component symbol and preserving the current timeline type
exports so consumers can continue importing Timeline from `@quent/components`.
ui/packages/@quent/components/src/timeline/Timeline.tsx (1)

379-386: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Broadcast every playhead state change.

usePlayheadLinePixel updates only from subscribePlayheadLine. In ui/packages/@quent/components/src/dag/DagPlayhead.tsx, the Arrow/Home/End handlers and the restart path update playheadTimeS without calling broadcastPlayheadLine. The playhead control can therefore move while this line remains stale. Broadcast those updates or subscribe the overlay to the canonical playhead state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/packages/`@quent/components/src/timeline/Timeline.tsx around lines 379 -
386, The playhead overlay can become stale because usePlayheadLinePixel depends
on broadcast updates that are missing from DagPlayhead state changes. Update the
Arrow/Home/End handlers and restart path in DagPlayhead to call
broadcastPlayheadLine whenever playheadTimeS changes, or make the overlay
subscribe directly to the canonical playhead state while preserving existing
chart-ready behavior.
🧹 Nitpick comments (1)
ui/src/components/DataFlowOverlay.test.tsx (1)

267-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a focused chart-level test for playhead conversion.

DataFlowOverlay.test.tsx mounts DagPlayhead, not Timeline or TimelineController. Its DOM assertions do not cover usePlayheadLinePixel or coexistence of the solid playhead line with the dotted ECharts pointer. Keep the producer assertion here and cover those behaviors in a Timeline-focused test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/DataFlowOverlay.test.tsx` around lines 267 - 295, Add a
focused Timeline-level test covering playhead line pixel conversion and
coexistence of the solid playhead line with the dotted ECharts pointer. Keep the
existing producer/broadcast assertions in the DataFlowOverlay test, and mount
the Timeline or TimelineController path so usePlayheadLinePixel and both
rendered indicators are exercised.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ui/packages/`@quent/components/src/index.ts:
- Around line 167-171: Restore the package-root named Timeline export in the
index.ts export list, using the existing Timeline component symbol and
preserving the current timeline type exports so consumers can continue importing
Timeline from `@quent/components`.

In `@ui/packages/`@quent/components/src/lib/timeline.utils.ts:
- Around line 529-535: Ensure every playheadTimeS state update, including
keyboard handling and restart paths in DagPlayhead, broadcasts the new value
through broadcastPlayheadLine so usePlayheadLinePixel stays synchronized. Prefer
centralizing the state update and broadcast in one helper, and preserve
hidePlayheadLine behavior when the playhead is cleared.

In `@ui/packages/`@quent/components/src/timeline/Timeline.tsx:
- Around line 379-386: The playhead overlay can become stale because
usePlayheadLinePixel depends on broadcast updates that are missing from
DagPlayhead state changes. Update the Arrow/Home/End handlers and restart path
in DagPlayhead to call broadcastPlayheadLine whenever playheadTimeS changes, or
make the overlay subscribe directly to the canonical playhead state while
preserving existing chart-ready behavior.

---

Nitpick comments:
In `@ui/src/components/DataFlowOverlay.test.tsx`:
- Around line 267-295: Add a focused Timeline-level test covering playhead line
pixel conversion and coexistence of the solid playhead line with the dotted
ECharts pointer. Keep the existing producer/broadcast assertions in the
DataFlowOverlay test, and mount the Timeline or TimelineController path so
usePlayheadLinePixel and both rendered indicators are exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 664dfcb4-346e-44b5-a93b-b1b04b64b7b3

📥 Commits

Reviewing files that changed from the base of the PR and between d5c3ceb and 2ec86c8.

📒 Files selected for processing (4)
  • ui/packages/@quent/components/src/index.ts
  • ui/packages/@quent/components/src/lib/timeline.utils.ts
  • ui/packages/@quent/components/src/timeline/Timeline.tsx
  • ui/src/components/DataFlowOverlay.test.tsx

broadcastHidePointer(null);
}

type PlayheadListener = (timestampMs: number | null) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use an atom for this to stay reactive without setting up a custom pub/sub system?

@cmatzenbach cmatzenbach Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can go ahead and move that to an atom instead of keeping the pub/sub block, that should make it cleaner.

One caveat though - the ECharts event listeners (dataZoom, finished) in usePlayheadLinePixel would still be necessary regardless, since the pixel position goes stale on zoom/resize even when the timestamp hasn't changed - that part of the complexity doesn't go away with the atom approach.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I just didn't want to set up the custom pub/sub + global to track if atoms do work for our purposes

onReady: onChartReady,
});

const playheadPixelX = usePlayheadLinePixel(instanceRef, 0, readyTick);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on the fence about this living in Timeline. To keep this component as generic as possible, I think i'd prefer this stay in ResourceTimeline.

Worth a quick experiment I think. If it's too much plumbing then I could see a playhead being part of this component, but we should confirm that this fails gracefully if the Timeline component is used elsewhere without any kind of playhead hooked up to it.

@cmatzenbach cmatzenbach Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's a good idea, and ended up being a fairly easy move. All we needed to do was add an optional onReady prop to Timeline, so anyone using it without playhead hooked up is unaffected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/packages/@quent/components/src/dag/DagPlayhead.tsx (1)

87-101: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancel queued pointer work before clearing the playhead line.

If pointermove queues the animation frame and pointerup occurs before it runs, the callback calls applyClientX after Line 100 clears the state. The overlay line then remains visible after the interaction ends.

Cancel rafRef.current and clear pendingClientXRef.current in handlePointerEnd before calling setPlayheadLineTimeMs(null).

Proposed fix
   const handlePointerEnd = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
     if (event.currentTarget.hasPointerCapture(event.pointerId)) {
       event.currentTarget.releasePointerCapture(event.pointerId);
     }
+    if (rafRef.current != null) {
+      cancelAnimationFrame(rafRef.current);
+      rafRef.current = null;
+    }
+    pendingClientXRef.current = null;
     setPlayheadLineTimeMs(null);
   }, [setPlayheadLineTimeMs]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/packages/`@quent/components/src/dag/DagPlayhead.tsx around lines 87 - 101,
Update handlePointerEnd to cancel any queued animation frame via rafRef and
clear pendingClientXRef before calling setPlayheadLineTimeMs(null), preventing
deferred applyClientX work after pointer interaction ends.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/packages/`@quent/components/src/lib/usePlayheadLinePixel.ts:
- Around line 31-32: Update the convertToPixel result handling in the playhead
pixel calculation to first require typeof pixel === 'number', then retain the
finite-number check before passing it to setPixelX; continue assigning null for
arrays, invalid numbers, or other unsupported results.

---

Outside diff comments:
In `@ui/packages/`@quent/components/src/dag/DagPlayhead.tsx:
- Around line 87-101: Update handlePointerEnd to cancel any queued animation
frame via rafRef and clear pendingClientXRef before calling
setPlayheadLineTimeMs(null), preventing deferred applyClientX work after pointer
interaction ends.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: cb4f9a7e-06a4-4d17-a7a5-b1bf6234630d

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec86c8 and bda76ef.

📒 Files selected for processing (7)
  • ui/packages/@quent/components/src/dag/DagPlayhead.tsx
  • ui/packages/@quent/components/src/lib/timeline.utils.ts
  • ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts
  • ui/packages/@quent/hooks/src/atoms/dataFlow.ts
  • ui/packages/@quent/hooks/src/dataFlow/dataFlowSelectors.ts
  • ui/packages/@quent/hooks/src/index.ts
  • ui/src/components/DataFlowOverlay.test.tsx
💤 Files with no reviewable changes (1)
  • ui/packages/@quent/components/src/lib/timeline.utils.ts

Comment thread ui/packages/@quent/components/src/lib/usePlayheadLinePixel.ts Outdated
cmatzenbach and others added 3 commits August 5, 2026 09:20
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
}, [gridOptions, minZoomSpanPct, xAxisOptions, yAxisOptions, seriesOptions]);

const isDraggingRef = useRef(false);
const onReadyRef = useRef(onReady);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why store this in a ref? Can we just call onReady(instance) below on L366?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking onChartReady only runs once per chart instance, but looking at it further, you're right, it's recreated on each render, so the ref is unnecessary.

@johallar johallar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙇 ty for all the updates

@cmatzenbach

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit 0c92602 into rapidsai:main Aug 5, 2026
15 checks passed
@cmatzenbach
cmatzenbach deleted the independent-play-head-indicator branch August 5, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(ui): implement independent play-head indicator

2 participants