Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions tutorials/progressive_globe.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Circle size = log(sample count). Color = dominant data source.
<div class="panel-section">
<div class="stats-compact">
<div class="stat-box"><span id="sPhase" class="val">Loading...</span><span class="lbl">Resolution</span></div>
<div class="stat-box"><span id="sPoints" class="val">0</span><span class="lbl">Clusters</span></div>
<div class="stat-box"><span id="sPoints" class="val">0</span><span id="sPointsLbl" class="lbl">Clusters</span></div>
<div class="stat-box"><span id="sSamples" class="val">0</span><span class="lbl">Samples</span></div>
<div class="stat-box"><span id="sTime" class="val">-</span><span class="lbl">Load Time</span></div>
</div>
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`);

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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`);

Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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));
Expand Down