Skip to content

[ENHANCEMENT] Rework canvas pan/zoom and add on-screen zoom - #922

Open
PhilippMatthes wants to merge 3 commits into
perses:mainfrom
PhilippMatthes:feat/canvas-view-zoom
Open

PhilippMatthes wants to merge 3 commits into
perses:mainfrom
PhilippMatthes:feat/canvas-view-zoom

Conversation

@PhilippMatthes

Copy link
Copy Markdown
Contributor

Description

Follow-up from #891 and perses/perses#4634

This reworks the display panel so that it supports panning and zooming not only for regular mouses but also for trackpads and devices without a scroll wheel (like the Apple Magic Mouse I use). The implementation is inspired by how other tools like Grafana handle in-panel movements:

  • Pan by dragging the left mouse button anywhere on the canvas (middle-button drag still pans too).
  • Zoom with Ctrl/Cmd + wheel, or a trackpad pinch (which the browser delivers as a Ctrl+wheel). A plain, unmodified wheel no longer zooms — it scrolls the surrounding dashboard page, so a Magic Mouse swipe behaves as expected.
  • On-screen controls (zoom in / out / fit-to-view / reset) in a small overlay in the bottom-right, as a fallback for other devices or ways to use the canvas.

The editor (edit mode) is unchanged: it keeps middle-button pan and Ctrl/Cmd+wheel zoom, leaving the left button and plain wheel free for node move, edge drag, rubber-band selection, and page scroll.

Two behaviors are preserved explicitly under the new left-button pan and clamped zoom:

  • Node navigation — a small clickDistance keeps a near-stationary left click from being treated as a pan, so a node's URL still opens.
  • Fit to view — fitView widens the scale-extent floor for very large graphs so fit-to-view frames the whole graph.
  • Left-button panning relies on touch-action: none / user-select: none on the panel SVG so the browser's native drag/selection doesn't preempt the pan.

The gesture predicate (shouldStartGesture) and the zoom-limit logic (isAtScaleLimit) are pure, exported, and unit-tested (d3-zoom is ESM-only and awkward to drive through the DOM).

Screenshots

The only visual addition is the zoom-controls overlay (zoom in / out / fit / reset) in the bottom-right of the display panel; the graph rendering itself is unchanged. See this screenshot:

Screenshot 2026-11-06 at 16 12 09

Checklist

  • Pull request has a descriptive title and context useful to a reviewer.
  • Pull request title follows the [<catalog_entry>] <commit message> naming convention using one of the
    following catalog_entry values: FEATURE, ENHANCEMENT, BUGFIX, BREAKINGCHANGE, DOC,IGNORE.
  • All commits have DCO signoffs.

UI Changes

  • Changes that impact the UI include screenshots and/or screencasts of the relevant changes.
  • Code follows the UI guidelines.

Give the read-only canvas display panel Grafana-style zoom/pan via a new
`interactionMode` option on `useZoom`. The editor and display panel share this
hook, so the panel had inherited the editor's gesture constraints despite
having no editing gestures to protect, making a plain view unintuitive.

Interaction model (matching Grafana's node graph):

- Pan by dragging the left mouse button (view mode also keeps middle-button
  pan); the editor keeps its middle-button-only pan.
- Zoom with ctrl/meta + wheel (or a trackpad pinch, which the browser delivers
  as a ctrl wheel). A plain wheel no longer zooms, so a Magic Mouse / two-finger
  swipe scrolls the surrounding dashboard page instead of zooming the panel.
- Discoverable on-screen controls (zoom in/out, fit, reset) via a new
  `ZoomControls` overlay, since ctrl+wheel is not obvious.

Supporting changes:

- Add `zoomIn`/`zoomOut` (d3-zoom `scaleBy` about the panel center) and
  `scale`/`canZoomIn`/`canZoomOut` to `useZoom`, exposed through `ZoomContext`.
- Keep node URL navigation working under left-button pan via `clickDistance`,
  and let `fitView` widen the scale-extent floor so fit-to-view frames large
  graphs. Extract `shouldStartGesture` and `isAtScaleLimit` as pure, tested
  functions (d3-zoom is ESM-only and awkward to drive through the DOM).

Signed-off-by: Philipp Matthes <p.matthes@sap.com>
Co-authored-by: Markus Wieland <markus.wieland@sap.com>
@PhilippMatthes
PhilippMatthes marked this pull request as ready for review November 6, 2026 15:24
@PhilippMatthes
PhilippMatthes requested a review from a team as a code owner November 6, 2026 15:24
@PhilippMatthes
PhilippMatthes requested review from jgbernalp and removed request for a team November 6, 2026 15:24
@ibakshay
ibakshay requested a balanced review from Copilot November 7, 2026 19:54

Copilot AI 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.

🟡 Changes recommended

The zoom controls incorrectly disable zooming out within the effective extent, and the panel blocks native touch scrolling without providing touch panning.

2 open issues
What changed in this PR

Updates canvas navigation with display-mode panning and visible zoom controls, building on the data-driven graph work while preserving editor gestures.

Changes:

  • Enables left/middle-button panning in display mode and Ctrl/Cmd+wheel or trackpad-pinch zoom.
  • Adds zoom-in, zoom-out, fit-to-view, and reset controls with bounded zoom.
  • Extends the zoom context and test fixtures, and adds gesture-filter and scale-limit tests.
File Description
canvas/​src/​test-utils/​hookWrapper.tsx Adds stubs for the expanded zoom context.
canvas/​src/​hooks/​useZoom.ts Adds interaction modes, zoom bounds, and button actions.
canvas/​src/​hooks/​useZoom.test.ts Tests gesture filtering and scale-limit detection.
canvas/​src/​contexts/​ZoomContext.tsx Exposes zoom actions, scale, and availability flags.
canvas/​src/​components/​panel/​ZoomControls.tsx Adds the on-screen zoom toolbar.
canvas/​src/​components/​panel/​CanvasPanel.tsx Integrates display-mode navigation and controls.
canvas/​src/​components/​editor/​EditorItemsPanel.tsx Supplies the expanded context while retaining editor mode.

Review effort: Balanced

Comment thread canvas/src/hooks/useZoom.ts Resolved
- Stop forcing touch-action: none on the display panel SVG. Pan/zoom is
  mouse-only (shouldStartGesture rejects touch events), so suppressing
  touch-action only broke page scroll for touchscreen users swiping over
  the panel without giving them canvas panning in return. Let the browser
  keep its default touch handling.

- Track the effective scale extent in React state. fitView can widen the
  lower bound to frame a very large graph; the zoom-controls' disabled
  flags now derive from that live extent instead of the static constant,
  so zoom-out re-enables correctly after zooming in from a fitted scale
  below the configured floor. resetPan and remount restore the configured
  extent.

- Add unit coverage for the widened-extent and reset behavior.

Signed-off-by: Philipp Matthes <p.matthes@sap.com>
@PhilippMatthes

Copy link
Copy Markdown
Contributor Author

I hope I addressed the feedback appropriately. @ibakshay can you trigger the bot again so we make sure I didn't miss anything?

@ibakshay
ibakshay requested review from jgbernalp and a balanced review from Copilot and removed request for jgbernalp November 11, 2026 03:44

Copilot AI 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.

🔵 Needs a closer look

At the canvas zoom limits, Ctrl+wheel or trackpad pinch gestures can zoom the entire dashboard page because their default behavior is not prevented.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Prevent modified-wheel page zoom at scale limits

canvas/​src/​hooks/​useZoom.ts:145

With the new finite scale extent, d3-zoom ignores a fresh wheel gesture directed past a zoom limit without calling preventDefault(). Consequently, starting Ctrl+wheel or a trackpad pinch while already at the minimum or maximum scale can zoom the entire dashboard page instead of remaining confined to the canvas. Cancel accepted modified-wheel events even when the scale cannot change, while continuing to leave plain wheel events untouched.

2 resolved since last review

Review effort: Balanced

…limits

d3-zoom only calls preventDefault on a modified (ctrl/meta) wheel that
actually changes the transform. With the finite view-mode scaleExtent, a
ctrl+wheel or trackpad pinch directed past the min/max scale clamps to an
unchanged value and d3-zoom bails before preventing the default, so the
browser zooms the whole dashboard page instead of staying confined to the
canvas.

Attach a non-passive native wheel listener on the panel SVG that cancels
the default for the modified wheels we claim, regardless of whether the
scale changes, while leaving a plain wheel untouched so it keeps scrolling
the surrounding page. The listener is detached via the callback ref's
cleanup. The decision lives in an exported pure predicate
(shouldPreventWheelDefault) with unit coverage, plus integration tests
that dispatch wheel events at the scale limit and after ref teardown.

Signed-off-by: Philipp Matthes <p.matthes@sap.com>
@PhilippMatthes

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review — addressed the "Prevent modified-wheel page zoom at scale limits" finding in 3bc29f9.

You're right: d3-zoom's wheeled only calls preventDefault when a modified wheel actually changes the transform. With the finite view-mode scaleExtent, a ctrl/⌘+wheel or trackpad pinch directed past the min/max clamps to an unchanged scale and d3-zoom hits its t.k === k early-return before noevent(event), so the browser's default page zoom fires.

Fix: a non-passive native wheel listener on the panel SVG now cancels the default for the modified wheels we claim, whether or not the scale changes, while a plain wheel is left untouched so it keeps scrolling the surrounding page. The listener is detached in the callback ref's cleanup. The decision is an exported pure predicate (shouldPreventWheelDefault) with unit coverage, plus integration tests that dispatch a ctrl+wheel at the max scale limit (asserting defaultPrevented) and after ref teardown.

@PhilippMatthes

Copy link
Copy Markdown
Contributor Author

@ibakshay let the bot go zoop pls

@PhilippMatthes

Copy link
Copy Markdown
Contributor Author

Related to #858 btw

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.

2 participants