Skip to content

explorer: heatmap mutual-exclusivity (phase 3) + self-refresh loop fix (#233)#242

Merged
rdhyee merged 2 commits into
isamplesorg:mainfrom
rdhyee:feat/heatmap-mode-exclusive
May 28, 2026
Merged

explorer: heatmap mutual-exclusivity (phase 3) + self-refresh loop fix (#233)#242
rdhyee merged 2 commits into
isamplesorg:mainfrom
rdhyee:feat/heatmap-mode-exclusive

Conversation

@rdhyee
Copy link
Copy Markdown
Contributor

@rdhyee rdhyee commented May 28, 2026

This branch carries two related #233 heatmap improvements:

1. Heatmap mutually exclusive with marker layers (phase 3)

Previously the heatmap painted an imagery layer on top of the cluster/point dots, so two layers told contradictory spatial stories at once — the "dots-vs-hotspots disagreement." Now the heatmap stands alone: turning it on hides the markers; off restores whichever collection the altitude-driven mode wants.

  • New applyLayerVisibility() — single source of truth for .show on h3Points/samplePoints (aside from the one-time initializer). heatmap on ⇒ both hidden; off ⇒ cluster→h3Points / point→samplePoints.
  • enterPointMode/exitPointMode and the heatmap toggle route through it.
  • syncFacetNote() gains a heatmap conjunct: the "filters apply at sample zoom level" apology is false when the heatmap shows filtered density directly, so it's hidden while heatmap is on. Boot ?heatmap=1 inherits all of this.

2. Stop the heatmap self-refresh loop (tolerance dedupe)

RY observed the heatmap re-rendering repeatedly with no user input. Cause: the overlay refreshes on Cesium moveEnd, keyed off computeViewRectangle. With Cesium World Terrain enabled, the camera height keeps settling after a move (terrain-collision as tiles stream in) + inertial drift; the old exact-key dedupe (toFixed(4), ~11 m) let that jitter through → re-render → terrain nudge → moveEnd → loop. High-altitude views never looped (coarse/stable terrain) — matching the intermittent symptom.

Fix: refreshHeatmap() dedupes on a 2%-of-span tolerance against the last successfully-rendered view + filter, instead of exact-key equality. Sub-meaningful jitter is ignored; real pans/zooms render. The snapshot is deliberately not cleared on moveStart, so jitter firing full moveStart/moveEnd cycles is still measured against the committed overlay and can't re-arm the loop — and can't wedge, since any meaningful move exceeds tolerance.

  • lngDelta() folds the longitude delta to [0,180] so antimeridian-adjacent views aren't misread as a ~357° move.
  • Skip branch restores a truthful "rendered from N samples" status (incl. zero-sample).
  • viewer._heatmapSkips counts tolerance skips so the regression test proves the skip path ran.

Tests

tests/playwright/heatmap-overlay.spec.js10/10 green locally:

  • phase 3: markers hide/restore on toggle (cluster + point modes); #facetNote hidden when heatmap on; heatmap=1 boot hides markers.
  • loop fix: a sub-threshold nudge increments the skip counter, moves the camera, yet does NOT re-render (overlay intact); a ~6° move does re-render.

Provenance

Claude (plan + implementation), Codex 2-round review on each change (all findings resolved). Verified visually + Playwright; loop fix confirmed live by RY on local build.

🤖 Generated with Claude Code

rdhyee and others added 2 commits May 28, 2026 14:01
 phase 3)

Heatmap was an orthogonal overlay painting on top of the cluster/point
dots, producing the dots-vs-hotspots disagreement RY flagged 2026-05-27:
two layers telling contradictory spatial stories at once.

Phase 3 makes heatmap stand alone. Add a single applyLayerVisibility()
helper — now the ONLY writer of `.show` on h3Points/samplePoints — that
hides both marker collections while the heatmap is on and restores the
altitude-driven mode's collection when it's off. enterPointMode/
exitPointMode and the heatmap toggle handler all route through it.

Also hide the #facetNote apology while the heatmap is on: the note
("filters apply at sample zoom level") is false when the heatmap shows
filtered density directly. syncFacetNote() gains a heatmap conjunct.

Tests: 2 new specs in heatmap-overlay.spec.js (markers hide/restore on
toggle; facetNote hidden when heatmap on). Full heatmap suite green (8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lesorg#233)

RY observed the heatmap re-rendering repeatedly with no user input. Cause:
the overlay refreshes on Cesium `moveEnd`, keyed off `computeViewRectangle`.
With Cesium World Terrain enabled, the camera height keeps settling for a
beat after a move (terrain-collision adjustment as tiles stream in) and
inertia drifts post mouse-up — each tiny settle fired `moveEnd`, the old
exact-key dedupe (`toFixed(4)`, ~11 m) let the jitter through, the re-render
nudged terrain again, and the loop sustained. High-altitude views never
looped (coarse, stable terrain) — consistent with the symptom being
intermittent and zoom/region dependent.

Fix: refreshHeatmap() now dedupes on a 2%-of-span tolerance against the
last SUCCESSFULLY-rendered view + filter (heatmapLastBounds /
heatmapLastFilterHash), instead of exact-key equality. Sub-meaningful
jitter stays under the threshold and is ignored; real pans/zooms exceed it
and render. The snapshot is deliberately NOT cleared on moveStart, so
jitter that fires full moveStart/moveEnd cycles is still measured against
the committed overlay and can't re-arm the loop — and it can't wedge,
because any meaningful move always exceeds tolerance.

Details (Codex 2-round review):
- lngDelta() folds the longitude delta to [0,180] so antimeridian-adjacent
  views (wrapped cLng≈180 vs settled cLng≈-177) aren't read as a ~357° move.
- skip-branch restores a truthful "rendered from N samples" status (gated on
  heatmapLastBounds, so zero-sample overlays restore too) instead of leaving
  a moveStart "waiting for camera" stuck.
- viewer._heatmapSkips counts tolerance skips so the regression test can
  PROVE the skip branch ran (not a vacuous "unchanged timestamp" assertion).

Test: new regression spec — a sub-threshold nudge increments the skip
counter, moves the camera, yet does NOT re-render (overlay intact); a ~6°
move does re-render. Full heatmap suite green (10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rdhyee rdhyee changed the title explorer: heatmap mutually exclusive with marker layers (#233 phase 3) explorer: heatmap mutual-exclusivity (phase 3) + self-refresh loop fix (#233) May 28, 2026
@rdhyee rdhyee merged commit db3e1e8 into isamplesorg:main May 28, 2026
1 check passed
@rdhyee rdhyee deleted the feat/heatmap-mode-exclusive branch May 28, 2026 23:02
rdhyee added a commit to rdhyee/isamplesorg.github.io that referenced this pull request May 29, 2026
…samplesorg#243)

Additive 'collection' dimension: filter the explorer to a named SamplingSite
label (e.g. OpenContext 'PKAP Survey Area'). Precomputes site membership via
the wide-parquet Sample->Event->Site traversal into two new R2 files; touches
none of the existing facet files. Rebased onto main so it sits cleanly on top
of the merged isamplesorg#242 heatmap work (disjoint regions, no conflict).

- scripts/build_collections.py: builds collections.parquet + sample_collections
  .parquet. Unnests BOTH relationship arrays (multi-event/multi-site safe),
  counts DISTINCT pids, orders membership by collection_id for row-group
  pruning. PKAP=15,446 verified; both files live on data.isamples.org.
- explorer.qmd: dual-UX collection facet (top-N checkboxes + search-the-tail),
  ?collection= URL param wired through the existing facet lifecycle and the
  facetFilterSQL() chokepoint (2nd subquery against sample_collections.parquet).
- collections.qmd: Featured Collections page uses identity-based &collection=.
- EXPLORER_STATE.md, data.qmd: document the new param and files.
- tests/test_collections.py: page + facet-DOM checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant