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));