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
83 changes: 72 additions & 11 deletions explorer.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -1368,8 +1368,10 @@ zoomWatcher = {
const POINT_BUDGET = DEFAULT_POINT_BUDGET;

// Viewport cache: avoid re-querying same area
let cachedBounds = null; // { south, north, west, east }
let cachedData = null; // array of rows
let cachedBounds = null; // { south, north, west, east }
let cachedData = null; // array of rows
let cachedTotalCount = null; // real in-view count (issue #206)
let cachedCapReached = false; // whether the LIMIT was hit (issue #206)

// --- H3 cluster loading (existing logic) ---
//
Expand Down Expand Up @@ -1505,9 +1507,14 @@ zoomWatcher = {
const bounds = getViewportBounds();
if (!bounds) return;

// If viewport is within cached area, just re-render from cache
// If viewport is within cached area, just re-render from cache.
// Re-apply the cached count/cap state to the stat boxes so panning
// inside the padded cache window doesn't leave stale figures on
// screen (#206 Codex review).
if (isWithinCache(bounds) && cachedData) {
renderSamplePoints(cachedData, bounds);
const total = cachedTotalCount != null ? cachedTotalCount : cachedData.length;
updateStats('Samples', cachedData.length, total, null, 'Samples Rendered', 'Samples in View');
return;
}

Expand All @@ -1527,14 +1534,22 @@ zoomWatcher = {
performance.mark('sp-s');
// facetFilterSQL() returns a portable `pid IN (...)` predicate,
// so the same query works whether or not facet filters are active.
const query = `
SELECT pid, label, source, latitude, longitude,
place_name, result_time
FROM read_parquet('${lite_url}')
//
// ORDER BY pid (issue #206): without explicit ordering, which
// POINT_BUDGET-worth of rows the LIMIT returns is undefined and
// can differ across browsers/sessions for the same query.
const whereClause = `
WHERE latitude BETWEEN ${padded.south} AND ${padded.north}
AND longitude BETWEEN ${padded.west} AND ${padded.east}
${sourceFilterSQL('source')}
${facetFilterSQL()}
`;
const query = `
SELECT pid, label, source, latitude, longitude,
place_name, result_time
FROM read_parquet('${lite_url}')
${whereClause}
ORDER BY pid
LIMIT ${POINT_BUDGET}
`;
const data = await db.query(query);
Expand All @@ -1548,15 +1563,55 @@ zoomWatcher = {
return;
}

// Cache the padded bounds + data
// Real in-view count (issue #206). Without this, the "Samples in
// View" counter caps at POINT_BUDGET, silently understating dense
// regions (Cyprus/Polis: real count 23,421 vs displayed 5,000).
// Only query when we hit the cap — when LIMIT returned fewer
// rows than the budget, that count IS the real count and a
// second query would be wasteful.
let totalCount = data.length;
let capReached = false;
if (data.length >= POINT_BUDGET) {
try {
const countRow = await db.query(`
SELECT count(*) AS n
FROM read_parquet('${lite_url}')
${whereClause}
`);
if (myReqId !== requestId) return; // stale guard
totalCount = Number(countRow[0]?.n ?? data.length);
capReached = totalCount > data.length;
} catch(err) {
// Stale guard before any state mutation/logging:
// a newer request may have started while count was in
// flight (Codex review of PR #210).
if (myReqId !== requestId) return;
// Don't fail the whole load if the count query fails;
// just fall back to the displayed-count behavior.
console.warn("Real-count query failed; falling back to rendered count:", err);
}
}

// Cache the padded bounds + data + count state so cache-hit
// pans preserve the same stat-box display (Codex review).
cachedBounds = padded;
cachedData = Array.from(data);
cachedTotalCount = totalCount;
cachedCapReached = capReached;

renderSamplePoints(cachedData, bounds);

updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`, 'Samples in View', 'Samples 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`);
// Stat boxes: LEFT (sPoints) always shows rendered count under
// "Samples Rendered", RIGHT (sSamples) always shows real in-view
// total under "Samples in View". Stable labels even when both
// numbers are equal (Codex review preference: avoids label
// flipping as the user zooms across the cap boundary).
updateStats('Samples', cachedData.length, totalCount, `${(elapsed/1000).toFixed(1)}s`, 'Samples Rendered', 'Samples in View');
const phaseMsg = capReached
? `${totalCount.toLocaleString()} samples in view (showing ${cachedData.length.toLocaleString()} — zoom in for more). Click one for details.`
: `${cachedData.length.toLocaleString()} individual samples. Click one for details.`;
updatePhaseMsg(phaseMsg, 'done');
console.log(`Point mode: rendered ${cachedData.length} of ${totalCount} samples in ${elapsed.toFixed(0)}ms${capReached ? ' (cap reached)' : ''}`);

} catch(err) {
if (myReqId !== requestId) return;
Expand Down Expand Up @@ -1614,6 +1669,8 @@ zoomWatcher = {
if (pushHistory !== false) history.pushState(null, '', buildHash(viewer));
cachedBounds = null;
cachedData = null;
cachedTotalCount = null;
cachedCapReached = false;

// Restore cluster stats with viewport count
const bounds = getViewportBounds();
Expand Down Expand Up @@ -1883,6 +1940,8 @@ zoomWatcher = {
if (applied) await tryEnterPointModeIfNeeded();
} else {
cachedBounds = null;
cachedTotalCount = null;
cachedCapReached = false;
await loadViewportSamples();
}
refreshFacetCounts();
Expand Down Expand Up @@ -1948,6 +2007,8 @@ zoomWatcher = {
writeQueryState();
if (mode === 'point') {
cachedBounds = null;
cachedTotalCount = null;
cachedCapReached = false;
await loadViewportSamples();
}
refreshFacetCounts();
Expand Down
Loading