A plotly failure inside the WebEngine page was invisible from Python - the
react promise just rejected and the canvas looked frozen with no evidence.
PlotlyCanvas now installs a QWebEnginePage that forwards
javaScriptConsoleMessage into the Console dock with the right severity
(error/warning/info), so the next silent failure is diagnosable.
Three defects in the view-preservation path could leave the plot frozen
(no updates, no orbit, stale colours):
1. The merge injected raw _fullLayout objects - including undefined when a
push landed before the previous react resolved - and plotly validates
layouts, so one bad value made every later react fail permanently.
currentView() now deep-copies and validates eye/center/up and each
range, and mergeView() is wrapped so it can never block an update.
2. Updates were not serialized: overlapping Plotly.react calls on one graph
div left it unresponsive. otkoUpdate now queues and coalesces (one react
at a time, latest payload wins), and logs instead of failing silently.
3. A style change re-framed the camera, so re-colouring yanked a view the
user had orbited. Colour/opacity ride the traces and background/outline
ride the layout, so set_style is now a non-framing push.
Verified in the browser: a user orbit survives colour changes, 30 rapid
preview updates land on the final value and stay responsive, and deleting
the live camera no longer wedges the plot.
Plotly.react resets any scene attribute the incoming layout omits, so every
selection change snapped the camera back to the default and re-fit the
ranges - the pinned framing was meaningless. Data-only pushes now pass
preserveView and html.py merges the live camera and axis ranges into the
incoming layout; only an explicit re-frame sends computed framing.
Also fixes the ordering bug where the framing flag was consumed before the
not-ready early return, which dropped the very first framing on the floor
(show_project runs before loadFinished).
Verified in the browser: user orbit + zoom survive selection and working
plane updates, while reset_camera re-frames. Regression-tested headlessly
by asserting the emitted preserveView flag.