Skip to content

Commit 9e7c605

Browse files
rdhyeeclaude
andauthored
Add URL deep-links, viewport cluster count, and bug fixes (#51)
- Shareable URL state via hash params (lat/lng/alt/heading/pitch/mode/pid) - Share View button with clipboard copy - Browser back/forward navigation support - Viewport cluster count: "in view / total" format with dateline-safe logic - Fix: cluster-click query no longer references missing 'description' column - Fix: startup crash from _initialHash initialization order - Fix: clusterCount undefined in exitPointMode Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a24d363 commit 9e7c605

File tree

1 file changed

+57
-12
lines changed

1 file changed

+57
-12
lines changed

tutorials/progressive_globe.qmd

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Circle size = log(sample count). Color = dominant data source.
9999
<div class="panel-section">
100100
<div class="stats-compact">
101101
<div class="stat-box"><span id="sPhase" class="val">Loading...</span><span class="lbl">Resolution</span></div>
102-
<div class="stat-box"><span id="sPoints" class="val">0</span><span class="lbl">Clusters</span></div>
102+
<div class="stat-box"><span id="sPoints" class="val">0</span><span id="sPointsLbl" class="lbl">Clusters</span></div>
103103
<div class="stat-box"><span id="sSamples" class="val">0</span><span class="lbl">Samples</span></div>
104104
<div class="stat-box"><span id="sTime" class="val">-</span><span class="lbl">Load Time</span></div>
105105
</div>
@@ -195,12 +195,13 @@ function buildHash(v) {
195195
}
196196
197197
// === Helpers: update DOM imperatively (no OJS reactivity) ===
198-
function updateStats(phase, points, samples, time) {
198+
function updateStats(phase, points, samples, time, pointsLabel) {
199199
const s = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; };
200200
s('sPhase', phase);
201-
s('sPoints', points.toLocaleString());
202-
s('sSamples', samples.toLocaleString());
203-
s('sTime', time);
201+
s('sPoints', typeof points === 'string' ? points : points.toLocaleString());
202+
s('sSamples', typeof samples === 'string' ? samples : samples.toLocaleString());
203+
if (time != null) s('sTime', time);
204+
if (pointsLabel) s('sPointsLbl', pointsLabel);
204205
}
205206
206207
function updatePhaseMsg(text, type) {
@@ -485,11 +486,15 @@ phase1 = {
485486
});
486487
}
487488
489+
// Cache cluster data for viewport counting
490+
viewer._clusterData = Array.from(data);
491+
viewer._clusterTotal = { clusters: data.length, samples: totalSamples };
492+
488493
performance.mark('p1-end');
489494
performance.measure('p1', 'p1-start', 'p1-end');
490495
const elapsed = performance.getEntriesByName('p1').pop().duration;
491496
492-
updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`);
497+
updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`, 'Global Clusters');
493498
updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${totalSamples.toLocaleString()} samples. Zoom in for finer detail.`, 'done');
494499
console.log(`Phase 1: ${data.length} clusters in ${elapsed.toFixed(0)}ms`);
495500
@@ -510,6 +515,7 @@ zoomWatcher = {
510515
let currentRes = 4;
511516
let loading = false;
512517
let requestId = 0; // stale-request guard
518+
// clusterDataCache stored on viewer._clusterData (set by phase1 and loadRes)
513519
514520
// Hysteresis thresholds to avoid flicker
515521
const ENTER_POINT_ALT = 120000; // 120 km → enter point mode
@@ -550,11 +556,18 @@ zoomWatcher = {
550556
});
551557
}
552558
559+
// Cache for viewport counting
560+
viewer._clusterData = Array.from(data);
561+
viewer._clusterTotal = { clusters: data.length, samples: total };
562+
553563
performance.mark(`r${res}-e`);
554564
performance.measure(`r${res}`, `r${res}-s`, `r${res}-e`);
555565
const elapsed = performance.getEntriesByName(`r${res}`).pop().duration;
556566
557-
updateStats(`H3 Res${res}`, data.length, total, `${(elapsed/1000).toFixed(1)}s`);
567+
// Show viewport count immediately
568+
const bounds = getViewportBounds();
569+
const inView = countInViewport(bounds);
570+
updateStats(`H3 Res${res}`, `${inView.clusters.toLocaleString()} / ${data.length.toLocaleString()}`, inView.samples.toLocaleString(), `${(elapsed/1000).toFixed(1)}s`, 'In View / Total');
558571
updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${total.toLocaleString()} samples. ${res < 8 ? 'Zoom in for finer detail.' : 'Zoom closer for individual samples.'}`, 'done');
559572
560573
currentRes = res;
@@ -579,6 +592,22 @@ zoomWatcher = {
579592
};
580593
}
581594
595+
// --- Count clusters visible in current viewport (from cached array) ---
596+
function countInViewport(bounds) {
597+
const cache = viewer._clusterData;
598+
if (!bounds || !cache || cache.length === 0) return { clusters: 0, samples: 0 };
599+
const { south, north, west, east } = bounds;
600+
const wrapLng = west > east; // dateline crossing
601+
let clusters = 0, samples = 0;
602+
for (const row of cache) {
603+
if (row.center_lat < south || row.center_lat > north) continue;
604+
if (wrapLng ? (row.center_lng < west && row.center_lng > east) : (row.center_lng < west || row.center_lng > east)) continue;
605+
clusters++;
606+
samples += row.sample_count;
607+
}
608+
return { clusters, samples };
609+
}
610+
582611
// --- Check if viewport is within cached bounds ---
583612
function isWithinCache(bounds) {
584613
if (!cachedBounds || !bounds) return false;
@@ -638,7 +667,7 @@ zoomWatcher = {
638667
639668
renderSamplePoints(cachedData, bounds);
640669
641-
updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`);
670+
updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`, 'In View');
642671
updatePhaseMsg(`${cachedData.length.toLocaleString()} individual samples. Click one for details.`, 'done');
643672
console.log(`Point mode: ${cachedData.length} samples in ${elapsed.toFixed(0)}ms`);
644673
@@ -696,10 +725,16 @@ zoomWatcher = {
696725
cachedBounds = null;
697726
cachedData = null;
698727
699-
// Restore cluster stats
700-
let clusterCount = viewer.h3Points.length;
701-
updateStats(`H3 Res${currentRes}`, clusterCount, '—', '—');
702-
updatePhaseMsg(`${clusterCount.toLocaleString()} clusters. Zoom closer for individual samples.`, 'done');
728+
// Restore cluster stats with viewport count
729+
const bounds = getViewportBounds();
730+
const inView = countInViewport(bounds);
731+
const total = viewer._clusterTotal;
732+
if (total) {
733+
updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), '—', 'In View / Total');
734+
} else {
735+
updateStats(`H3 Res${currentRes}`, viewer.h3Points.length, '—', '—', 'Global Clusters');
736+
}
737+
updatePhaseMsg(`${inView.clusters.toLocaleString()} clusters in view. Zoom closer for individual samples.`, 'done');
703738
console.log('Exited point mode');
704739
}
705740
@@ -739,6 +774,16 @@ zoomWatcher = {
739774
}
740775
}
741776
777+
// Update viewport cluster count (cluster mode only; point mode already shows viewport count)
778+
if (mode === 'cluster' && viewer._clusterData) {
779+
const bounds = getViewportBounds();
780+
const inView = countInViewport(bounds);
781+
const total = viewer._clusterTotal;
782+
if (total) {
783+
updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), null, 'In View / Total');
784+
}
785+
}
786+
742787
// Update URL hash (replaceState for continuous movement)
743788
if (!viewer._suppressHashWrite) {
744789
history.replaceState(null, '', buildHash(viewer));

0 commit comments

Comments
 (0)