Repository navigation
Optimize pan/zoom performance and metadata lookups - #5
Merged
marota merged 10 commits intoFeb 15, 2026
Merged
Conversation
The pan/zoom was sluggish because every wheel/mouse event triggered a full React render cycle just to update the SVG viewBox attribute. For large SVGs injected via dangerouslySetInnerHTML, React had to examine the entire component tree on each frame despite nothing changing. Key optimizations: - Bypass React state for viewBox during active pan/zoom: update the DOM attribute directly via refs, sync back to React state only on interaction end/pause (debounced wheel, mouseup) - Stabilize event listener registration: depend only on [active] not [viewBox], eliminating 60+/sec listener teardown/setup cycles - Cache SVG element ref to avoid querySelector on every event - Throttle mouse drag via requestAnimationFrame (one DOM update per display frame) - Conditional rendering for inactive SVG tabs: unmount the hidden tab's SVG DOM tree, halving memory and layout overhead - Replace regex-based SVG string manipulation with DOMParser for the boostSvgForLargeGrid function, avoiding multi-MB temporary strings - Index metadata with Maps for O(1) zoomToElement lookups instead of linear Array.find scans https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
On large grids, the browser was laying out and painting thousands of foreignObject labels, edge-info flow values, and edge text labels that are completely unreadable at full zoom-out. This was the main cause of slow initial rendering. Fix: use CSS display:none to hide text elements by default on large grids (marked with data-large-grid attribute by boostSvgForLargeGrid). When the user zooms in past a threshold (viewBox < 3750 SVG units), toggle the text-hidden class off to reveal labels. The toggle is a single classList.toggle() in the applyViewBox hot path — near-zero cost. A useLayoutEffect applies text-hidden before the first paint to prevent any flash of unreadable text during initial load. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
The fixed TEXT_VISIBILITY_MAX_VIEWBOX (3750 SVG units) didn't work for large grids where zoomToElement computes viewBoxes of 4000-6000 units for a contingency neighborhood. Text stayed hidden even when zoomed in. Fix: use a relative threshold — show text when viewing < 50% of the original viewBox (i.e., zoomed in at least 2x). This scales with grid size automatically: zoomToElement zooms to 10-30% of the grid, so text now always appears on element focus. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
The data-large-grid attribute (which enables text-hiding) was set at the same ratio threshold (>1.5) as the element boost. This caused medium grids like Dijon (ratio ~1.6) to hide text at full zoom even though it was perfectly readable, breaking their previous behavior. Fix: set data-large-grid only at ratio > 3 (viewBox > 3750), where 25px text renders at ~8px on screen and is genuinely unreadable. Medium grids (ratio 1.5-3) still get the boost for zoom-in readability but keep text always visible — matching the pre-optimization behavior. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
Three fixes: 1. Raise boost threshold from ratio > 1.5 to ratio > 3 so small/medium grids (like Dijon) keep native pypowsybl sizing. Only genuinely large grids (viewBox > 3750) get the text/circle/edge-info boost and the text-hidden toggle. 2. Switch to N-1 tab immediately on contingency selection instead of waiting for the diagram to load. The user sees "Generating N-1 Diagram..." while the fetch runs. The auto-zoom effect now defers lastZoomState update until the SVG container is ready, ensuring the zoom fires correctly when the diagram loads rather than being consumed by a failed attempt on an empty container. 3. Add AC load flow convergence check with DC fallback in get_n1_diagram (backend). If AC doesn't converge, falls back to DC — matching the existing pattern in run_analysis. This prevents NaN bus voltages from producing labels like "kV / -3°" with missing voltage magnitudes. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
…ence banner
Four fixes:
1. Normalized boost formula: sqrt(ratio/3) instead of sqrt(ratio), so
grids just above the threshold get barely any visual change (ratio=4
→ boost 1.15x instead of 2.0x). Decoupled text-hidden (data-large-grid)
to ratio > 6 so medium-large grids keep text always visible. Added
console.log diagnostic for the ratio to help tune thresholds.
2. Reverted DC load flow fallback in get_n1_diagram — DC only computes
angles/power, not voltage magnitudes, so running it after failed AC
overwrites partial-but-useful AC results with emptier values. Now uses
AC results as-is and returns lf_converged + lf_status in the response.
3. Fixed auto-zoom flashing on N tab: added guard to skip branch-triggered
zoom when activeTab is still 'n' (batched setActiveTab('n-1') not yet
committed in same render cycle). The zoom correctly defers until the
N-1 tab is active and the diagram SVG is loaded.
4. Added convergence warning banner in N-1 tab — shows a yellow info bar
with the load flow status when AC didn't converge.
https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
Three fixes: 1. Pan/zoom broken on N-1 after auto-zoom: the usePanZoom event registration effect only depended on [active], so events attached to the "Generating..." placeholder div (no SVG). When the diagram loaded and replaced the placeholder, events were stale. Added initialViewBox to the dependency array — events re-register when the diagram loads and the SVG container appears. 2. Multi-bus voltage levels (like PYMONP3): replaced per-circle r attribute scaling with group-level transforms. Now scales the entire parent <g> around the circle center, so both the outer circle AND inner path sectors (bus color segments) scale uniformly. 3. Boost threshold now uses voltage level count instead of viewBox ratio. Grids with < 500 VLs get no boost at all — the SVG is returned unchanged from pypowsybl. This definitively fixes small/medium grids regardless of their viewBox size. The VL count is passed from the API response through processSvg to boostSvgForLargeGrid. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
Console logs now show vlCount, viewBox dimensions, SVG width/height attributes, container dimensions, and effective scaling factor. This helps diagnose why small grids may render differently from native pypowsybl output — the most likely cause is a viewBox change from a pypowsybl library update rather than a code issue. https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv
marota
pushed a commit
that referenced
this pull request
Feb 22, 2026
Addresses remaining lag when panning/zooming on large grid diagrams, building on the foundation from PR #5. Key optimizations: - Batch wheel zoom events through rAF (was applying every wheel event directly, now accumulates scale factor and applies once per frame) - Cache getScreenCTM() between frames to avoid layout recalculations - Disable non-scaling-stroke during active interaction via CSS class toggle (the browser recalculates stroke widths for every SVG element on each viewBox change — this is the primary lag source) - Disable pointer-events on SVG children during pan/zoom to skip hit-testing across thousands of elements - Suppress drop-shadow filters during interaction (expensive compositing) - Add CSS containment (contain: layout style paint) to SVG container - Add hysteresis to text-visibility threshold (hide at 55%, show at 45%) to prevent flicker when zooming near the boundary https://claude.ai/code/session_01E4knUJ3qRtqNKmqCT799Bn
marota
pushed a commit
that referenced
this pull request
May 2, 2026
Adds two layers of regression guards covering all five
recommendations from `docs/proposals/ui-design-critique.md`:
1. Seven new static invariants in `scripts/check_invariants.py`
(Layer 4):
- notices_panel_in_sidebar — AppSidebar mounts <NoticesPanel>
- action_feed_no_dismissable_warning_state — ActionFeed lost
setShowActionDictWarning / setShowRecommenderWarning
- overload_panel_uses_monitoring_hint — OverloadPanel exposes
`monitoringHint`, not the legacy banner props
- diagram_legend_on_each_diagram_tab — VisualizationPanel wires
<DiagramLegend tabId="n|n-1|action">
- nad_overload_halo_capped_at_zoom — App.css caps overload halo
stroke at 24px on detail-zoom via vector-effect: non-scaling-
stroke
- design_token_gate_blocks_inline_hex — FRONTEND_HEX_LITERAL_MAX
stays at 0
- action_card_progressive_disclosure_gated_by_isViewing —
ActionCard renders the disclosure subtree under {isViewing &&
Total: 10 → 17 invariants. All green.
2. New `frontend/src/uxConsistency.test.tsx` (19 specs) — runtime
contract checks rendering the real components rather than
mocks. One describe block per recommendation:
- #1 design tokens: scans inline `style` attributes for raw hex
literals and asserts none leak from OverloadPanel / AppSidebar
- #2 progressive disclosure: ActionCard at-rest hides the
disclosure subtree; isViewing=true reveals it
- #3 halo cap: re-asserts the App.css rule (cheaper feedback
loop than the static script)
- #4 warning tier: OverloadPanel without monitoringHint shows
no inline yellow banner; ActionFeed at default state has no
warning-styled descendant; AppSidebar surfaces the
<NoticesPanel> pill iff notices.length > 0
- #5 diagram legend: each tab in VisualizationPanel mounts a
legend pill when its SVG is loaded, omits it when absent
- source-text invariants: cross-checks the file-level
contracts so a refactor that drops a recommendation fails
this file too
1182 → 1201 specs. All passing; ESLint clean; code-quality
gate clean.
https://claude.ai/code/session_01EYXsLY6hKRncRxj3KTHERk
marota
added a commit
that referenced
this pull request
May 5, 2026
``check_gesture_sequence.py``'s handler walker only matched the bare ``useCallback(`` wrapper when locating an arrow-function body. The auto-generated standalone bundle emits the namespaced form ``reactExports.useCallback(`` (vite/rollup imports the hook through a namespace object), so handlers like ``handleRunAnalysis`` that the React source defines via ``useCallback`` were not detected on the standalone side. The script then fell back to call-site scanning, whose +2000-char forward window is too narrow to span the ``runAnalysisStep2Stream`` body — missing the ``analysis_step2_completed`` ``recordCompletion`` and producing a spurious gesture-#5 failure. Accept an optional ``<ident>.`` namespace prefix in front of ``useCallback`` so both the bare and dotted forms resolve to the arrow-function body. Local run now reports 30/30 gestures passing (was 29/30) and gestures 2, 4, 6, 7, 9–14 also resolve via the stricter ``handler:*`` route instead of the looser ``callsite:*`` fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
marota
pushed a commit
that referenced
this pull request
Jun 18, 2026
Frontend coverage is now measurable (node_modules + @vitest/coverage-v8), so set a real floor instead of leaving it report-only. vite.config.ts gains `coverage.thresholds` a few points below the measured baseline (stmt 73.7 / branch 70 / func 73.8 / line 76.3 → floors 70/65/70/73), validated locally (npm run test:coverage exits 0). CI now runs the suite WITH coverage as the gate (replacing the plain run — same tests + the floor) in both pipelines. Backend coverage stays report-only: it can't be measured offline (several tests need the real recommender), so its `--cov-fail-under` floor must be read off the first green CI run, then ratcheted. Documented in §19. https://claude.ai/code/session_0185GQf52QDpMM4zRDmGVN8X
marota
pushed a commit
that referenced
this pull request
Jun 18, 2026
The first green CI run after §19 reported backend coverage 78% (no-graphviz job: TOTAL 4771/1063 = 78%, 849 passed). Set the floor in pyproject.toml ([tool.coverage.report] fail_under = 72) — 6 points below baseline, the same margin as the frontend floor — so `pytest --cov` now gates in both pipelines. Both coverage halves gate now; §20 documents it. https://claude.ai/code/session_0185GQf52QDpMM4zRDmGVN8X
marota
referenced
this pull request
in marota/Co-Study4Grid
Jul 8, 2026
Closes the still-open half of headline #5 (the trust model no longer matches deployment) and tightens the deploy pipeline. Lockdown profile: - COSTUDY4GRID_LOCKDOWN (read into main._LOCKDOWN) disables the desktop- era filesystem RPCs — POST /api/config-file-path, save/list/load-session, GET /api/pick-path — with a 403 {code: LOCKED_DOWN} via _reject_when_locked_down(). They assume a local operator and would give an anonymous Space visitor read/write access to the container. - The read-only app config (GET user-config / config-file-path) stays open so the SPA still boots. Unset locally → behaviour unchanged. - LOCKED_DOWN is a stable code in the D2 envelope (403 → LOCKED_DOWN) and the frontend ApiErrorCode union. The Dockerfile sets the flag. Deploy hardening: - deploy-huggingface.yml is now test-gated: it fires on workflow_run of a SUCCESSFUL "Tests" run on main (not a bare merge), and checks out that tested commit. workflow_dispatch stays as the rollback path. - Each deploy tags the shipped commit on origin (space-deploy-<ts>-<sha>) — the only durable record, since the Space push is force-pushed and history-free. Rollback = re-dispatch from a prior tag. Reproducible Python closure (pinned lockfile) is documented as a follow-up in deployment-trust.md / SETUP.md — it must be resolved on the image's Python 3.10, so shipping a dev-3.11 lockfile would be wrong. Tests: TestLockdownProfile (403 + LOCKED_DOWN when locked, read-only config still reachable, normal 200 when unlocked). OpenAPI snapshot unaffected. Docs: new deployment-trust.md + index; review tracker (D7 mostly-done, headline #5, dimensions 9 & 12); CLAUDE.md trees; SETUP.md rollback + lockfile sections; .env.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am26qvtnHoY1qnmssaSQam Signed-off-by: Antoine Marot <amarot91@gmail.com>
marota
pushed a commit
that referenced
this pull request
Jul 9, 2026
Lands the cheap halves of the two still-open headline issues (#4 invisible failures, #5 trust model) plus dead-code cleanup: - QW4: surface useAnalysis.error through StatusToasts (error || analysis.error, cleared on contingency-clear) so an analysis failure is no longer a silent dead spinner; the last silent SLD-preview catch now setError's too. Guarded by a new App-integration test. - QW3: CORS default "*" -> loopback dev origins (localhost/127.0.0.1 on :5173/:4173); the wildcard is now explicit opt-in via CORS_ALLOWED_ORIGINS="*", closing the drive-by cross-origin read of a localhost backend. - QW7: _safe_session_dir rejects "..", path separators and absolute session names before any filesystem write (resolve()+relative_to() backstop mirroring the /results/pdf guard); applied to save-session + load-session. Guarded by TestSessionPathTraversal. - QW9: delete inspect_action.py, the dead expert_backend/requirements.txt stub, and the unused react-zoom-pan-pinch dependency (+ lockfile and doc follow-through). The tracked Overflow_Graph/*.html is deliberately kept: it is a skip-guarded regression fixture and untracking it would silently drop that CI coverage. Docs (CLAUDE.md trees, README, .env.example) reconciled with the CORS default change, the removed files, and the react-zoom-pan-pinch removal (the "rendered inside react-zoom-pan-pinch" claims corrected to the actual usePanZoom viewBox mechanism); review tracker updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am26qvtnHoY1qnmssaSQam Signed-off-by: Antoine Marot <amarot91@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR significantly improves the performance of the SVG diagram viewer by optimizing the pan/zoom interaction loop and metadata-based element lookups. The changes bypass React's render cycle during active interactions and replace O(n) array searches with O(1) Map-based lookups.
Key Changes
Pan/Zoom Performance Optimization
viewBoxRefto update SVG viewBox directly via DOM without triggering React renders during active panning/zoomingsvgElRefto avoid repeatedquerySelectorcalls on every event handler invocationMetadata Indexing
buildMetadataIndex()function that converts node/edge arrays into Maps indexed by equipmentId and svgId.find()calls (O(n)) with Map.get()calls (O(1)) for element lookupsuseMemoto avoid rebuilding on every renderSVG Scaling Refactor
DOM Efficiency
visibility: hiddento conditional rendering with&&, reducing DOM weight when inactiveactivetab, notviewBox, preventing unnecessary re-registrationsImplementation Details
usePanZoomhook now maintains two viewBox representations:viewBoxRef(mutable, hot path) and React state (settled, for downstream consumers)activeRefto avoid stale closure issues while keeping the effect dependency array minimaluseLayoutEffectensures the ref is updated when diagram content changessetViewBoxPublicAPI updates both the ref and React state immediately for programmatic changes (e.g., zoom-to-element)https://claude.ai/code/session_01N3CzfYViwD4Xhs9aFnZafv