# 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 `
` inside a root-level ``. 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.ts`** — `attachVlInteractions(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 `` as a group ``. 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 `` 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.