Skip to content

feat(html): run physics while dragging a node in graph.html - #4864

Open
afrianluthfan wants to merge 1 commit into
Graphify-Labs:v8from
afrianluthfan:feat/html-drag-physics
Open

afrianluthfan wants to merge 1 commit into
Graphify-Labs:v8from
afrianluthfan:feat/html-drag-physics

Conversation

@afrianluthfan

Copy link
Copy Markdown

What does this PR do?

graph.html turns physics off once the initial layout stabilizes. After that, a dragged node stays wherever it is dropped. Its neighbours do not react, so a node can be dropped on top of another or dragged far away from its group.

This PR turns physics back on while a node is dragged:

  • Nodes pushed too close repel each other (existing forceAtlas2Based repulsion and avoidOverlap).
  • Edges pull a node dragged too far back toward its neighbours (existing springs).
  • Physics turns off again on the next stabilized event after the drag ends.
let draggingNode = false;
network.on('dragStart', params => {
  if (!params.nodes.length) return;
  draggingNode = true;
  network.setOptions({ physics: { enabled: true } });
});
network.on('dragEnd', () => { draggingNode = false; });
network.on('stabilized', () => {
  if (!draggingNode) network.setOptions({ physics: { enabled: false } });
});

Notes from the vis-network 9.1.6 source (the pinned version):

  • Network.setOptions({physics}) only emits startSimulation. It does not re-run stabilization or refit the view, so the camera does not jump on drag.
  • dragStart also fires for view pans, with params.nodes empty. The guard keeps pans from enabling physics.
  • stabilized can fire while a node is still held, if the user pauses mid-drag. draggingNode keeps physics on until the drag ends.

Solver parameters and the initial stabilizationIterationsDone freeze are unchanged.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Physics runs only during the initial layout and while a node drag is settling. It is off at rest and during view pans.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant. (Feature, no bug to reproduce.)
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Limitations:

  • A drag simulates every visible node. On graphs near the viz node limit the page does more work until the layout settles.
  • Orphan nodes have no springs, so the repulsion from a nearby drop can push them away from the group.

How was this tested?

tests/test_html_drag_physics.py (3 tests): the initial freeze is still present, dragStart returns before enabling physics when no node is dragged, and stabilized disables physics only when no node is held. 2 of the 3 fail on upstream/v8.

I also checked it manually in headless Chromium (Playwright) on a hub with 7 leaves plus a second small community:

Action Result
After load physics off
Drag leaf onto another leaf physics on during drag, hub moved 30px in response; leaf distance 110px at drop, 150px after settle; physics off after settle
Drag leaf ~430px away and release distance to hub 344px at release, 33px after settle; physics off after settle
Pan the view physics stays off, nodes do not move
uv run --frozen pytest tests/test_html_drag_physics.py -q       # 3 passed
uv run --frozen pytest tests -q                                 # 8654 passed, 8 failed
uv run --frozen ruff check graphify/exporters/html.py tests/test_html_drag_physics.py
uv run --frozen pyright graphify/exporters/html.py              # 0 errors

The 8 failures are in tests/test_ext_tagged_id_clash.py and tests/test_skill_no_backtick_substitution.py. They fail the same way on a clean upstream/v8 checkout in my environment, so they are unrelated to this change.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (Not touched.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

🤖 Generated with Claude Code

graph.html turns physics off after the initial layout, so a dragged node
stays wherever it is dropped and its neighbours do not react.

Dragging a node now turns physics back on. Nodes pushed too close repel
each other, and edges pull a node dragged too far back toward its
neighbours. Physics turns off again on the next 'stabilized' event after
the drag ends. Panning the view leaves physics off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @afrianluthfan. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 2 changed function(s) (1 sampled, not proven).

Formal verification. 1 change(s) alter behavior, breaking input(s) attached. PR-changed functions: 2/2 verified (0 proven, 1 may-equivalent, 1 distinguished) · 0 not verified.

Behavior changes: \_html\_script changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_html\_script behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"nodes\_json":"''","edges\_json":"''","legend\_json":"'\\\\x00'"\}, the old code produced '\<script\>\\nconst RAW\_NODES = ;\\nconst RAW\_EDGES = ;\\nconst LEGEND = \\x00;\\n\\n// HTML\-escape helper — prevents XSS when injecting graph data into innerHTML\\nfunction esc\(s\) \{\\n return… but the new code produces '\<script\>\\nconst RAW\_NODES = ;\\nconst RAW\_EDGES = ;\\nconst LEGEND = \\x00;\\n\\n// HTML\-escape helper — prevents XSS when injecting graph data into innerHTML\\nfunction esc\(s\) \{\\n return…. Paste that input straight into a regression test.


Graphify review — findings

Adds physics back on when a node is dragged in the exported graph.html, so nodes pushed together repel and edges pull an over-dragged node back toward its neighbours. A stabilized handler turns physics off again once the layout settles after the drop, and the initial freeze after stabilizationIterationsDone is unchanged. The re-enable is gated on params.nodes being non-empty in the dragStart handler, which is meant to keep plain view pans from waking the simulation, though a pan while a node is selected is the case that gate has to get right.

Worth a look

  • View pan with a node already selected re-enables physics — graphify/exporters/html.py:260 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 518 functions depend on the 18 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: to_html() — 31 callers, 11 callees

Verification — 518 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 80 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

8 of 470 test file(s) selected (2%) via static blast radius.

  • tests/test_export.py — impact
  • tests/test_export_direction.py — impact
  • tests/test_html_drag_physics.py — impact, changed-test
  • tests/test_html_script_breakout.py — impact
  • tests/test_html_theme.py — impact
  • tests/test_labeling.py — impact
  • tests/test_pipeline.py — impact
  • tests/test_watch.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Behavior changes: \_html\_script changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_html\_script behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"nodes\_json":"''","edges\_json":"''","legend\_json":"'\\\\x00'"\}, the old code produced '\<script\>\\nconst RAW\_NODES = ;\\nconst RAW\_EDGES = ;\\nconst LEGEND = \\x00;\\n\\n// HTML\-escape helper — prevents XSS when injecting graph data into innerHTML\\nfunction esc\(s\) \{\\n return… but the new code produces '\<script\>\\nconst RAW\_NODES = ;\\nconst RAW\_EDGES = ;\\nconst LEGEND = \\x00;\\n\\n// HTML\-escape helper — prevents XSS when injecting graph data into innerHTML\\nfunction esc\(s\) \{\\n return…. Paste that input straight into a regression test.

No difference found (not proven): No behavior difference found in to\_html (not a proof).

The verifier ran both versions of to\_html on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 1 more finding(s) on lines outside this diff (see the check run).

@afrianluthfan

Copy link
Copy Markdown
Author

Re the advisory "View pan with a node already selected re-enables physics":

Checked this against vis-network 9.1.6: on a view pan, InteractionHandler.onDragStart calls generateClickEvent("dragStart", ..., emptySelection=true), which sets params.nodes = [] regardless of the current selection. So the guard holds with a node selected. I also verified it in Chromium: pan right after dragging (node still selected) leaves network.physics.physicsEnabled === false.

This branch has not been deployed

No deployments
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.

1 participant