Skip to content

fix(timeline): keep scroll zoom anchored to cursor#860

Merged
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/timeline-zoom-anchor-f09d
Jun 8, 2026
Merged

fix(timeline): keep scroll zoom anchored to cursor#860
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/timeline-zoom-anchor-f09d

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep vertical wheel input as zoom-only so the hovered timestamp stays anchored under the cursor
  • preserve dominant horizontal wheel panning with a small wrapper handler
  • stop horizontal wheel events before vis-timeline can also treat them as zoom input

Root cause

With horizontalScroll: true, vis-timeline first emits the wheel event to the range zoom handler, then its horizontal-scroll path pans the same vertical wheel event. That makes zoom behave like a mix of cursor anchor plus center/range drift.

Fixes #847.

Verification

  • npm run lint -- src/visualizations/VisTimeline.vue
  • npm run build (passes; existing bundle-size and Sass/Babel deprecation warnings only)

@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 34.38%. Comparing base (afa1e61) to head (fe3dec5).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #860   +/-   ##
=======================================
  Coverage   34.38%   34.38%           
=======================================
  Files          36       36           
  Lines        2114     2114           
  Branches      408      408           
=======================================
  Hits          727      727           
  Misses       1308     1308           
  Partials       79       79           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a zoom-anchor drift bug in the vis-timeline component that occurred because horizontalScroll: true caused vis-timeline to process vertical wheel events twice — once as zoom and once as pan. The fix adds preferZoom: true to suppress vis-timeline's built-in panning and replaces it with a capture-phase wheel handler that intercepts only horizontally-dominant events, normalises deltaX across deltaMode variants, and stops the event before vis-timeline's bubble-phase handler can reprocess it.

  • preferZoom: true is added to the timeline options so vertical wheel input zooms around the cursor without also shifting the window centre.
  • A new onHorizontalWheel method (capture phase, passive: false) intercepts events where |deltaX| > |deltaY|, normalises for DOM_DELTA_LINE/DOM_DELTA_PAGE, computes a proportional setWindow offset, then calls stopImmediatePropagation to prevent vis-timeline from also acting on the same event.
  • The listener is registered in mounted and cleaned up in beforeDestroy.

Confidence Score: 5/5

The change is narrowly scoped to wheel-event routing in a single Vue component; the new capture-phase handler, deltaMode normalisation, and beforeDestroy cleanup are all correct, and preferZoom:true cleanly removes the double-processing that caused the reported drift.

The fix correctly intercepts only horizontally-dominant wheel events in the capture phase, normalises delta values across all three deltaMode variants, and prevents vis-timeline from reprocessing the same event. The listener lifecycle (add in mounted, remove in beforeDestroy) is handled properly. No correctness defects were found in the changed code.

No files require special attention.

Important Files Changed

Filename Overview
src/visualizations/VisTimeline.vue Adds preferZoom option and a capture-phase onHorizontalWheel handler with deltaMode normalisation and proper listener cleanup; logic is correct and the event lifecycle is handled cleanly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    W[WheelEvent fires on #visualization]
    W --> CP[Capture phase: onHorizontalWheel]
    CP --> COND{"|deltaX| > |deltaY|?"}
    COND -- "No (vertical dominates)" --> PASSTHROUGH[Return — event propagates normally]
    PASSTHROUGH --> VT_ZOOM[vis-timeline bubble-phase handler\npreferZoom:true → zoom around cursor]
    COND -- "Yes (horizontal dominates)" --> NORM["Normalise deltaX\n• DOM_DELTA_LINE → x40\n• DOM_DELTA_PAGE → x800\n• DOM_DELTA_PIXEL → unchanged"]
    NORM --> CALC["diff = (deltaX/120) x (windowSpan/20)"]
    CALC --> SW["timeline.setWindow(start+diff, end+diff)"]
    SW --> STOP["preventDefault + stopImmediatePropagation\n→ vis-timeline never sees event"]
Loading

Reviews (2): Last reviewed commit: "fix(timeline): normalize horizontal whee..." | Re-trigger Greptile

Comment thread src/visualizations/VisTimeline.vue Outdated
const currentWindow = this.timeline.getWindow();
const start = currentWindow.start.valueOf();
const end = currentWindow.end.valueOf();
const diff = (event.deltaX / 120) * ((end - start) / 20);

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.

P2 The panning delta is computed as event.deltaX / 120 without checking WheelEvent.deltaMode. When deltaMode === 1 (DOM_DELTA_LINE, which Firefox uses for tilt-wheel mice), deltaX is typically 13 lines per notch instead of 120 pixels, making deltaX / 120 roughly 0.0080.025 — effectively no visible panning. A deltaMode normalisation step (pixel ≈ 1×, line ≈ 40×, page ≈ 800×) keeps the formula consistent across browsers.

Suggested change
const diff = (event.deltaX / 120) * ((end - start) / 20);
// Normalise deltaX to CSS-pixel equivalents across deltaMode values
// (DOM_DELTA_PIXEL=0 ~1px, DOM_DELTA_LINE=1 ~40px, DOM_DELTA_PAGE=2 ~800px).
const PIXELS_PER_LINE = 40;
const PIXELS_PER_PAGE = 800;
let deltaXpx = event.deltaX;
if (event.deltaMode === 1) deltaXpx *= PIXELS_PER_LINE;
else if (event.deltaMode === 2) deltaXpx *= PIXELS_PER_PAGE;
const diff = (deltaXpx / 120) * ((end - start) / 20);

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.

Fixed in fe3dec5: normalized deltaX for DOM_DELTA_LINE and DOM_DELTA_PAGE before applying the pan formula. Verified with npm run lint -- src/visualizations/VisTimeline.vue and npm run build.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Ready for maintainer merge.

Rechecked head fe3dec51d56042c7cd01fd5b64cd2e6fd8aafa26: all GitHub checks pass, Codecov reports all modified coverable lines covered, and Greptile's latest summary is 5/5 on this head.

I attempted a guarded squash merge with --match-head-commit fe3dec51d56042c7cd01fd5b64cd2e6fd8aafa26, but TimeToBuildBob does not have MergePullRequest permission in ActivityWatch/aw-webui. A maintainer can merge it when ready.

@ErikBjare ErikBjare merged commit 53b1499 into ActivityWatch:master Jun 8, 2026
8 checks passed
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.

v0.14.0b1 Timeline zoom: scroll anchor doesn't follow cursor position

2 participants