Co-Study4Grid / docs /features /vl-disk-interactions.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
7.13 kB

Voltage-level disk interactions (NAD)

The voltage-level (VL) disks drawn by pypowsybl on the Network Area Diagram are directly interactive on every NAD tab — Network (N), Contingency and Remedial Action. Three gestures are wired onto each disk:

Gesture Effect
Hover Shows the VL name in a small floating tooltip — but only while the on-diagram VL labels are hidden (the 🏷 VL toggle). When the labels are visible the name is already drawn, so the tooltip stays out of the way.
Single-click (disk) Selects the VL: fills the bottom-left Inspect field with the VL id, auto-zooms / highlights it, and surfaces the 📄 SLD shortcut — exactly as typing the name into the Inspect box would.
Single-click (name box) Opens the VL's Single Line Diagram overlay directly — the name label next to the disk is a second, larger click target for the SLD.
Double-click (disk or name box) Opens the VL's Single Line Diagram overlay (the same entry point as the 📄 SLD button / onVlOpen).

This is the disk-driven complement to the Inspect search field: the field finds an asset by name; the disk finds it by pointing at it.

The whole disk is a target, even under a branch

A branch is often drawn ON TOP of a VL disk, which would otherwise steal the pointer hit-test from the disk beneath it. The handlers resolve this: when the direct hit-test lands on something that is not a VL (an occluding edge, or empty space), they fall back to document.elementsFromPoint and take the first VL disk / name box in the paint stack under the cursor. So the disk is interactive across its whole area regardless of what is painted over it. The fallback only runs on discrete pointer events (mousedown / hover-while-labels-hidden), never per frame, so the performance contract below is unchanged.

The name box is resolved through the metadata

pypowsybl renders each VL name as a <div class="nad-label-box" id="…"> inside a root-level <foreignObject class="nad-text-nodes">. The box id is the NAD metadata text-node svgId, which the metadata index (metaIndex.textNodesBySvgId, built from metadata.textNodes via each entry's vlNode link / equipmentId) maps to the same VL NodeMeta the disk resolves to. A delegated handler climbs from the click target to the first ancestor whose id is a known disk or text-node svgId.

Where it lives

  • frontend/src/utils/svg/vlInteractions.tsattachVlInteractions(container, metaIndex, handlers). The whole behaviour is one function returning a teardown.
  • frontend/src/App.tsx — a single effect binds the three NAD containers, mapping each gesture to the existing handlers:
    • onSelect(vlId)handleInspectQueryChangeFor(tab, vlId) (drives the per-tab Inspect query + auto-zoom in useDiagrams). Fired by a single-click on the disk.
    • onOpenSld(vlId)handleVlOpen(vlId)useSldOverlay.handleVlDoubleClick. Fired by a double-click on the disk / name box and by a single-click on the name box. The callbacks are held in refs (vlSelectRef / vlOpenSldRef / vlDisplayNameRef) so the listeners re-bind only when a diagram or its metadata index changes — never on an unrelated render, which would needlessly tear down the listeners and cancel an in-flight single-click timer.
  • frontend/src/App.css.svg-container .nad-vl-nodes, .svg-container .nad-label-box { cursor: pointer } is the (static) pointer-cursor affordance on both the disk and the name box.

onSelect / onOpenSld reuse pre-existing interaction events (inspect_query_changed, sld_overlay_opened), so the replay log stays complete with no new event types.

How a disk is resolved

pypowsybl renders each VL under <g class="nad-vl-nodes"> as a group <g id="{svgId}"><circle r="27.5"/>…</g>. The diagram metadata index (metaIndex.nodesBySvgId) maps that svgId to the VL's equipmentId (the VL id used everywhere else — voltageLevels, the SLD endpoints, the zoom-to-element lookup). A delegated handler climbs from the event target up to the first ancestor whose id is a known node svgId, yielding the VL.

Performance contract (do not regress)

Pan/zoom fluidity is the hard constraint — the NAD can carry ~5 000 VLs / ~100 k DOM nodes. The design is built around touching nothing per frame:

  • Event delegation. A fixed handful of listeners on the container (mouseover, mouseout, mousedown, click, dblclick), never one per node. A 5 000-VL grid costs the same to wire as a 5-VL one.
  • No mousemove / no per-frame work. The tooltip is positioned once on mouseover; the cursor is a static CSS rule.
  • Idle during gestures. usePanZoom adds .svg-interacting during a pan/zoom, which sets pointer-events: none on every SVG child (App.css), so none of the disk handlers fire mid-gesture. Pan/zoom is a direct viewBox rewrite (no React re-render per frame), and this layer adds nothing to it.

The click-vs-pan subtlety (regression-tested)

Because .svg-interacting flips pointer-events: none on SVG children the instant a press starts, by the time mouseup fires the disk is transparent and the browser retargets the resulting click / dblclick to the container (the common ancestor of the disk-mousedown and the container-mouseup). Resolving the VL from click.target would therefore always miss — this is exactly the bug where hover worked but clicks did nothing.

The fix: capture the VL on mousedown, whose hit-test still lands on the live disk (the cull class is only set by that handler, after the target is already fixed), and use the captured node in the click / double-click handlers. A press is told apart from a pan by pointer travel (DRAG_THRESHOLD_PX), and the single-click action is deferred VL_SINGLE_CLICK_DELAY_MS (250 ms) so a double-click pre-empts it.

Tests

  • frontend/src/utils/svg/vlInteractions.test.ts — unit coverage for the module: single-click → onSelect, double-click → onOpenSld (and single-click suppression), the container-retargeted click / dbl-click (the real-world pointer-events cull), the drag guard, hover-tooltip gating on nad-hide-vl-labels, displayName fallback, same-group mouseout, single-tooltip reuse, nested-child resolution, multi-VL disambiguation, teardown (listeners + tooltip + pending timer), and the no-op guards.
  • frontend/src/App.stateManagement.test.tsx (Voltage-level disk interaction wiring) — captures the handlers App passes to attachVlInteractions and invokes them, asserting onSelect flows into the shared Inspect query and onOpenSld opens the SLD overlay for the VL.

History

Supersedes the former native <title> tooltip injector (utils/svg/vlTitles.ts, applyVlTitles, removed): the native title only delivered hover-name; the unified layer delivers hover + click + double- click with an immediate, theme-aware tooltip and no double tooltip.