Skip to content

Conversation

@atif09
Copy link

@atif09 atif09 commented Dec 11, 2025

Enable scrolling to zoom in/out from anywhere on the graph container, not just over the graph nodes themselves, this matches the behavior of the geographic map mode

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #421

Description of Changes

  • Added wheel event listener that captures scroll events anywhere on the graph container, then dispatches a synthetic wheel event to the canvas element so ECharts' built in roam handler can process the zoom (the original implementation only listened to events directly on the canvas)
  • Users can now scroll to zoom in/out from any position on the graph, making the interface more intuitive and consistent with the geographic map mode

Screen recording

BEFORE

netjsongraph.1.mp4

AFTER

netjsongraph.mp4

…wisp#421

Enable scrolling to zoom in/out from anywhere on the graph container,
not just over the graph nodes themselves. This matches the behavior
of the geographic map mode.

Fixes openwisp#421

fix: add safety check for getDom in tests
@atif09 atif09 force-pushed the issues/421-enable-wheel-zoom-outside-graph branch from 9006bee to 1db2cc2 Compare December 21, 2025 17:46
@atif09 atif09 force-pushed the issues/421-enable-wheel-zoom-outside-graph branch from 6cc76fd to a588f58 Compare December 21, 2025 18:18
Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

I had tested this and it was working, which is good ✔️ !

  • Let's fix the QA checks, try the following:
pip install -U --force-reinstall "openwisp-utils[qa] @ https://github.com/openwisp/openwisp-utils/tarball/master"
yarn lint:fix
openwisp-qa-format
  • We also need at least 1 automated browser test for this to protect ourselves against regressions.

  • Please @coderabbitai review. @atif09 please follow up with its reviews.

@nemesifier
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

Walkthrough

This pull request adds wheel-based zoom handling for areas outside the graph/map drawable surface. When a wheel event occurs on the chart DOM container but outside the graph/map area, the code now prevents the default behavior, locates the inner canvas element, and dispatches a synthetic wheel event centered on the canvas to enable zoom operations. This extends zoom functionality to the entire chart container area. Additionally, minor formatting adjustments were made throughout the file for readability without altering existing behavior.

Sequence Diagram

sequenceDiagram
    actor User
    participant ChartDOM as Chart Container<br/>(DOM)
    participant Handler as Wheel Event<br/>Handler
    participant Canvas as Canvas<br/>(Graph/Map)

    User->>ChartDOM: Scroll wheel outside graph area
    ChartDOM->>Handler: Trigger wheel event
    Handler->>Handler: Check if outside graph/map
    Handler->>Handler: preventDefault()
    Handler->>Canvas: Find inner canvas element
    Handler->>Canvas: Create & dispatch synthetic<br/>wheel event (centered)
    Canvas->>Canvas: Process wheel event for zoom
    Canvas->>User: Update zoom level
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main change: enabling wheel zoom outside the graph area, which is the primary feature added in this PR.
Description check ✅ Passed The description covers the main change, references the closed issue (#421), includes screen recordings, and completes most checklist items. Only test and documentation updates are marked incomplete.
Linked Issues check ✅ Passed The PR fully addresses issue #421 by implementing wheel-based zoom handling outside the graph area through container-level event listening, matching the geographic map mode behavior as requested.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the wheel zoom feature and necessary formatting adjustments; no unrelated out-of-scope modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/js/netjsongraph.render.js:
- Around line 441-475: The wheel-forwarding handler must be made robust: stop
querying canvas via dom.querySelector("canvas") and instead get the ZRender
surface via self.echarts.getZr().dom (works for canvas and SVG), preserve and
forward all relevant event fields (deltaX, deltaY, deltaMode, ctrlKey, shiftKey,
altKey, metaKey, button, etc.) without inverting sign, and avoid
double-forwarding by detecting if the original event already originated on the
ZRender DOM (skip forwarding when e.target === zrDom or
zrDom.contains(e.target)). Also prevent listener accumulation by storing the
listener (e.g., this._wheelForwardHandler) and removing any previous listener
from dom before attaching a new one so it can be cleaned up on re-renders;
ensure you add the new listener with {passive:false} as before.
🧹 Nitpick comments (2)
src/js/netjsongraph.render.js (2)

2-7: Prettier is failing; please re-run formatter before merge

CI reports Prettier failures for this file; several hunks here are line-wrapping-only changes, so this is likely just a formatting mismatch with the repo’s Prettier config.


LGTM on the “wrap-only” changes, and the getBBoxData.call(self, ...) this-binding is a good safety fix

  • The graph series type selection refactor preserves behavior (Line 208-214).
  • Map node emphasis extraction/wrapping looks behavior-preserving (Line 276-280).
  • getBBoxData.call(self, ...) avoids accidental this issues if getBBoxData relies on this (Line 668-672).
  • Cluster click zoom increment is bounded cleanly (Line 738-745).
  • Duplicate-node warning is fine as-is (Line 856-858).

Also applies to: 208-214, 276-280, 397-400, 432-435, 668-672, 688-690, 708-710, 738-745, 856-858


674-681: Potentially incorrect “new links” filter: current logic can drop legitimate links

This filter keeps a link only if both its source is unseen and its target is unseen:

!sourceLinkSet.has(link.source) && !targetLinkSet.has(link.target)

That can wrongly reject a new edge where (for example) the source was seen before but the (source,target) pair is new. If the intent is “dedupe by edge”, consider keying by (source,target) instead of separate sets.

Suggested approach: dedupe by edge key
-const sourceLinkSet = new Set(self.data.links.map((l) => l.source));
-const targetLinkSet = new Set(self.data.links.map((l) => l.target));
+const linkKey = (l) => `${l.source}→${l.target}`;
+const linkKeySet = new Set(self.data.links.map(linkKey));

-const links = data.links.filter(
-  (link) =>
-    !sourceLinkSet.has(link.source) && !targetLinkSet.has(link.target),
-);
+const links = data.links.filter((link) => !linkKeySet.has(linkKey(link)));
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e503883 and a588f58.

📒 Files selected for processing (1)
  • src/js/netjsongraph.render.js
🧰 Additional context used
🪛 GitHub Actions: netjsongraph.js CI BUILD
src/js/netjsongraph.render.js

[warning] 1-1: Code style issues found. Prettier check failed for this file. Run 'openwisp-qa-format' or 'Prettier --write' to fix.


[error] 1-1: Prettier check failed! Hint: did you forget to run openwisp-qa-format?

Comment on lines +441 to +475
// Enable wheel zoom outside graph area
const dom = self.echarts.getDom && self.echarts.getDom();
if (dom) {
dom.addEventListener(
"wheel",
(e) => {
const rect = dom.getBoundingClientRect();
if (
e.clientX < rect.left ||
e.clientX > rect.right ||
e.clientY < rect.top ||
e.clientY > rect.bottom
) {
return;
}
e.preventDefault();
const canvas = dom.querySelector("canvas");
if (canvas) {
const r = canvas.getBoundingClientRect();
canvas.dispatchEvent(
new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
view: window,
clientX: r.left + r.width / 2,
clientY: r.top + r.height / 2,
deltaY: -e.deltaY,
deltaMode: e.deltaMode,
}),
);
}
},
{passive: false},
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd netjsongraph.render.js

Repository: openwisp/netjsongraph.js

Length of output: 97


🏁 Script executed:

wc -l src/js/netjsongraph.render.js

Repository: openwisp/netjsongraph.js

Length of output: 101


🏁 Script executed:

sed -n '441,475p' src/js/netjsongraph.render.js

Repository: openwisp/netjsongraph.js

Length of output: 1107


🏁 Script executed:

sed -n '400,550p' src/js/netjsongraph.render.js | head -n 150

Repository: openwisp/netjsongraph.js

Length of output: 4494


🏁 Script executed:

rg "graphRender" src/js/netjsongraph.render.js -A 2 -B 2

Repository: openwisp/netjsongraph.js

Length of output: 303


🏁 Script executed:

rg "graphRender\(" src/js/ -B 3 -A 3

Repository: openwisp/netjsongraph.js

Length of output: 952


🏁 Script executed:

rg "this.utils.graphRender\|this.graphRender" src/js/ -B 5 -A 2

Repository: openwisp/netjsongraph.js

Length of output: 50


🏁 Script executed:

rg "graphRender" src/js/netjsongraph.js -B 3 -A 3

Repository: openwisp/netjsongraph.js

Length of output: 921


🏁 Script executed:

rg "echarts.init\|setOption\|SVGRenderer\|CanvasRenderer" src/js/ -B 2 -A 2

Repository: openwisp/netjsongraph.js

Length of output: 50


🏁 Script executed:

rg "echarts\." src/js/netjsongraph.js -A 1 | head -n 50

Repository: openwisp/netjsongraph.js

Length of output: 421


🏁 Script executed:

sed -n '1,100p' src/js/netjsongraph.js

Repository: openwisp/netjsongraph.js

Length of output: 2808


🏁 Script executed:

rg "addEventListener.*wheel" src/js/ -B 5 -A 10

Repository: openwisp/netjsongraph.js

Length of output: 50


🏁 Script executed:

rg "\.render\(" src/js/ -B 2 -A 2

Repository: openwisp/netjsongraph.js

Length of output: 1210


🏁 Script executed:

rg "_containerWheelHandler\|removeEventListener.*wheel" src/js/ -B 2 -A 2

Repository: openwisp/netjsongraph.js

Length of output: 50


🏁 Script executed:

rg "removeEventListener" src/js/ -B 2 -A 2

Repository: openwisp/netjsongraph.js

Length of output: 50


🏁 Script executed:

rg "setData\|clear\|init" src/js/netjsongraph.core.js | head -n 30

Repository: openwisp/netjsongraph.js

Length of output: 50


Wheel forwarding handler has multiple functional risks (renderer mismatch, listener accumulation, and event fidelity)

Key concerns:

  • Renderer mismatch: ECharts can be initialized with SVG renderer (svgRender config option), making dom.querySelector("canvas") return null and silently failing to forward wheel events.
  • Listener accumulation: graphRender() is called on re-renders (data updates, mode switches) without removing previous listeners, adding duplicate wheel handlers.
  • Event fidelity: The synthetic event omits deltaX, modifier keys (ctrlKey, shiftKey, altKey, metaKey), and potentially other fields. The deltaY: -e.deltaY inversion is unexplained.
  • No double-zoom guard: If the wheel event originates on the actual ECharts surface, it can still bubble to dom and get forwarded again.

Use chart.getZr().dom instead of querying for canvas (works with both SVG and canvas renderers), store the handler for cleanup across re-renders, check if the event already targets the ZRender surface, and preserve all event fields.

🤖 Prompt for AI Agents
In @src/js/netjsongraph.render.js around lines 441 - 475, The wheel-forwarding
handler must be made robust: stop querying canvas via
dom.querySelector("canvas") and instead get the ZRender surface via
self.echarts.getZr().dom (works for canvas and SVG), preserve and forward all
relevant event fields (deltaX, deltaY, deltaMode, ctrlKey, shiftKey, altKey,
metaKey, button, etc.) without inverting sign, and avoid double-forwarding by
detecting if the original event already originated on the ZRender DOM (skip
forwarding when e.target === zrDom or zrDom.contains(e.target)). Also prevent
listener accumulation by storing the listener (e.g., this._wheelForwardHandler)
and removing any previous listener from dom before attaching a new one so it can
be cleaned up on re-renders; ensure you add the new listener with
{passive:false} as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[bug] Scrolling quirk in network graph mode

2 participants