diff --git a/tutorials/progressive_globe.qmd b/tutorials/progressive_globe.qmd index ffa49d5..26f5bd7 100644 --- a/tutorials/progressive_globe.qmd +++ b/tutorials/progressive_globe.qmd @@ -99,7 +99,7 @@ Circle size = log(sample count). Color = dominant data source.
Loading...Resolution
-
0Clusters
+
0Clusters
0Samples
-Load Time
@@ -195,12 +195,13 @@ function buildHash(v) { } // === Helpers: update DOM imperatively (no OJS reactivity) === -function updateStats(phase, points, samples, time) { +function updateStats(phase, points, samples, time, pointsLabel) { const s = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; }; s('sPhase', phase); - s('sPoints', points.toLocaleString()); - s('sSamples', samples.toLocaleString()); - s('sTime', time); + s('sPoints', typeof points === 'string' ? points : points.toLocaleString()); + s('sSamples', typeof samples === 'string' ? samples : samples.toLocaleString()); + if (time != null) s('sTime', time); + if (pointsLabel) s('sPointsLbl', pointsLabel); } function updatePhaseMsg(text, type) { @@ -485,11 +486,15 @@ phase1 = { }); } + // Cache cluster data for viewport counting + viewer._clusterData = Array.from(data); + viewer._clusterTotal = { clusters: data.length, samples: totalSamples }; + performance.mark('p1-end'); performance.measure('p1', 'p1-start', 'p1-end'); const elapsed = performance.getEntriesByName('p1').pop().duration; - updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`); + updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`, 'Global Clusters'); updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${totalSamples.toLocaleString()} samples. Zoom in for finer detail.`, 'done'); console.log(`Phase 1: ${data.length} clusters in ${elapsed.toFixed(0)}ms`); @@ -510,6 +515,7 @@ zoomWatcher = { let currentRes = 4; let loading = false; let requestId = 0; // stale-request guard + // clusterDataCache stored on viewer._clusterData (set by phase1 and loadRes) // Hysteresis thresholds to avoid flicker const ENTER_POINT_ALT = 120000; // 120 km → enter point mode @@ -550,11 +556,18 @@ zoomWatcher = { }); } + // Cache for viewport counting + viewer._clusterData = Array.from(data); + viewer._clusterTotal = { clusters: data.length, samples: total }; + performance.mark(`r${res}-e`); performance.measure(`r${res}`, `r${res}-s`, `r${res}-e`); const elapsed = performance.getEntriesByName(`r${res}`).pop().duration; - updateStats(`H3 Res${res}`, data.length, total, `${(elapsed/1000).toFixed(1)}s`); + // Show viewport count immediately + const bounds = getViewportBounds(); + const inView = countInViewport(bounds); + updateStats(`H3 Res${res}`, `${inView.clusters.toLocaleString()} / ${data.length.toLocaleString()}`, inView.samples.toLocaleString(), `${(elapsed/1000).toFixed(1)}s`, 'In View / Total'); updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${total.toLocaleString()} samples. ${res < 8 ? 'Zoom in for finer detail.' : 'Zoom closer for individual samples.'}`, 'done'); currentRes = res; @@ -579,6 +592,22 @@ zoomWatcher = { }; } + // --- Count clusters visible in current viewport (from cached array) --- + function countInViewport(bounds) { + const cache = viewer._clusterData; + if (!bounds || !cache || cache.length === 0) return { clusters: 0, samples: 0 }; + const { south, north, west, east } = bounds; + const wrapLng = west > east; // dateline crossing + let clusters = 0, samples = 0; + for (const row of cache) { + if (row.center_lat < south || row.center_lat > north) continue; + if (wrapLng ? (row.center_lng < west && row.center_lng > east) : (row.center_lng < west || row.center_lng > east)) continue; + clusters++; + samples += row.sample_count; + } + return { clusters, samples }; + } + // --- Check if viewport is within cached bounds --- function isWithinCache(bounds) { if (!cachedBounds || !bounds) return false; @@ -638,7 +667,7 @@ zoomWatcher = { renderSamplePoints(cachedData, bounds); - updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`); + updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`, 'In View'); updatePhaseMsg(`${cachedData.length.toLocaleString()} individual samples. Click one for details.`, 'done'); console.log(`Point mode: ${cachedData.length} samples in ${elapsed.toFixed(0)}ms`); @@ -696,10 +725,16 @@ zoomWatcher = { cachedBounds = null; cachedData = null; - // Restore cluster stats - let clusterCount = viewer.h3Points.length; - updateStats(`H3 Res${currentRes}`, clusterCount, '—', '—'); - updatePhaseMsg(`${clusterCount.toLocaleString()} clusters. Zoom closer for individual samples.`, 'done'); + // Restore cluster stats with viewport count + const bounds = getViewportBounds(); + const inView = countInViewport(bounds); + const total = viewer._clusterTotal; + if (total) { + updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), '—', 'In View / Total'); + } else { + updateStats(`H3 Res${currentRes}`, viewer.h3Points.length, '—', '—', 'Global Clusters'); + } + updatePhaseMsg(`${inView.clusters.toLocaleString()} clusters in view. Zoom closer for individual samples.`, 'done'); console.log('Exited point mode'); } @@ -739,6 +774,16 @@ zoomWatcher = { } } + // Update viewport cluster count (cluster mode only; point mode already shows viewport count) + if (mode === 'cluster' && viewer._clusterData) { + const bounds = getViewportBounds(); + const inView = countInViewport(bounds); + const total = viewer._clusterTotal; + if (total) { + updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), null, 'In View / Total'); + } + } + // Update URL hash (replaceState for continuous movement) if (!viewer._suppressHashWrite) { history.replaceState(null, '', buildHash(viewer));