Keep the view across a tab switch

Perspective's auto-pause is on by default, and pausing deletes the view:
AutoPauseState::apply() fires on the viewer's own IntersectionObserver and
on the document's visibilitychange, and set_pause(true) does
view_sub.take().delete(). Coming back is therefore not a redraw but
restore_and_render() -- a fresh view and a fresh traversal of the whole
grain -- which is the multi-second chug on every tab switch, and takes any
per-node expansion with it.

Nothing updates the table while the tab is hidden; every operation is
driven from this page. So the pause bought nothing and cost a rebuild.

Also drops a comment that still described the old set_depth re-apply on
refocus, whose machinery is long gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-19 09:08:01 -04:00
parent a028cdb79a
commit d14036d1b5
2 changed files with 31 additions and 6 deletions

View File

@ -272,6 +272,21 @@ gone:
`split_full` is still *read* on load, for layouts saved under the old scheme.
### Auto-pause is off
`<perspective-viewer>` auto-pauses by default: an `IntersectionObserver` on
itself plus the document's `visibilitychange` drive `AutoPauseState::apply()`,
and pausing **deletes the view** (`session.set_pause(true)` →
`view_sub.take().delete()`). Returning to the tab is therefore not a redraw but
`restore_and_render()` — a fresh view and a fresh traversal of the whole grain,
which on a large one is a multi-second chug on every tab switch, and takes any
per-node expansion with it.
`initViewer()` calls `viewer.setAutoPause(false)` right after `viewer.load()`.
Nothing updates the table while the tab is hidden — every operation is driven
from this page — so the pause bought nothing and cost a rebuild. The view is now
held while the tab is backgrounded.
### Still not solved: per-node expansion
Expanding one specific branch is view state with no config representation, and

View File

@ -259,12 +259,6 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
// the current selection, readable from event handlers that were registered once
const slicesRef = useRef([])
// Perspective's ROLLUP view contains every level of the hierarchy; view.set_depth()
// is the only thing hiding the deeper ones, and it lives on the view rather than in
// the saved config. The viewer rebuilds its view when it redraws which its
// Intersection/ResizeObserver triggers when you switch back to the tab and the
// fresh view has no depth set, so the whole tree appears expanded. Re-apply the
// depth we last set once the redraw has settled.
// perspective-click fires as a CustomEvent with no modifier state of its own,
// and carries no cell coordinates either, so record both from the mousedown that
// precedes it. The grid lives in a shadow root, so the real target is the head of
@ -822,6 +816,22 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
// that occurs when viewer.load(worker) + restore({ table: name }) can't resolve
// the named table in time.
await viewer.load(tableRef.current)
// Auto-pause is on by default, and pausing *deletes the view*:
// AutoPauseState::apply() fires on visibilitychange and on the viewer's own
// IntersectionObserver, and set_pause(true) does
// `view_sub.take().delete()`. Coming back is therefore not a redraw but
// `restore_and_render()` -- a fresh view, a fresh traversal of the whole
// grain -- which is the multi-second chug on every tab switch, and why any
// per-node expansion is gone when you return.
//
// Nothing updates the table while the tab is hidden (operations are driven
// from this page), so the pause buys nothing here and costs a rebuild.
// Guarded: it is a method on the custom element, absent on older builds.
try { if (viewer.setAutoPause) await viewer.setAutoPause(false) } catch (err) {
console.error('[setAutoPause]', err)
}
viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light')
if (!hideSplitTotal()) setTimeout(hideSplitTotal, 400)