Skip to content

Commit 52bd89b

Browse files
rdhyeeclaude
andcommitted
Major improvements to parquet_cesium visualization
- Added loading states for async operations with visual feedback - Improved error handling with loadData function using latest-only pattern - Added explanatory text for Path 1 and Path 2 sample queries - Fixed race conditions in data fetching with request tokens - Added loading indicators for geo record and sample queries - Improved user experience with clear loading/error states - Added detailed documentation of query chains for both paths 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 173a013 commit 52bd89b

1 file changed

Lines changed: 82 additions & 53 deletions

File tree

tutorials/parquet_cesium.qmd

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOi
3131
viewof parquet_path = Inputs.text({label:"Source", value:"https://storage.googleapis.com/opencontext-parquet/oc_isamples_pqg.parquet", width:"100%", submit:true});
3232
```
3333

34+
::: callout-warning
35+
#### Heads up: first interaction may be slow
36+
The first click or query can take a few seconds while the in‑browser database engine initializes and the remote Parquet file is fetched and indexed. Subsequent interactions are much faster because both the browser and DuckDB cache metadata and column chunks, so later queries reuse what was already loaded.
37+
:::
38+
3439
```{ojs}
3540
//| code-fold: true
3641
@@ -42,24 +47,30 @@ db = {
4247
}
4348
4449
45-
async function loadData(query, params=[], waiting_id=null) {
50+
async function loadData(query, params = [], waiting_id = null, key = "default") {
51+
// latest-only guard per key
52+
loadData._latest = loadData._latest || new Map();
53+
const requestToken = Symbol();
54+
loadData._latest.set(key, requestToken);
55+
4656
// Get loading indicator
47-
const waiter = document.getElementById(waiting_id);
48-
if (waiter) {
49-
waiter.hidden = false;
50-
}
57+
const waiter = waiting_id ? document.getElementById(waiting_id) : null;
58+
if (waiter) waiter.hidden = false;
59+
5160
try {
5261
// Run the (slow) query
53-
const _results = await db.query(query, ...params);
62+
const _results = await db.query(query, params);
63+
// Ignore stale responses
64+
if (loadData._latest.get(key) !== requestToken) return null;
5465
return _results;
5566
} catch (error) {
56-
if (waiter) {
57-
waiter.innerHtml = `<pre>${error}</pre>`;
67+
if (waiter && loadData._latest.get(key) === requestToken) {
68+
waiter.innerHTML = `<pre>${error}</pre>`;
5869
}
5970
return null;
6071
} finally {
61-
// Hide the waiter (if there is one)
62-
if (waiter) {
72+
// Hide the waiter (if there is one) only if latest
73+
if (waiter && loadData._latest.get(key) === requestToken) {
6374
waiter.hidden = true;
6475
}
6576
}
@@ -68,7 +79,7 @@ async function loadData(query, params=[], waiting_id=null) {
6879
locations = {
6980
// get the content form the parquet file
7081
const query = `SELECT pid, latitude, longitude FROM nodes WHERE otype='GeospatialCoordLocation'`;
71-
const data = await loadData(query, [], "loading_1");
82+
const data = await loadData(query, [], "loading_1", "locations");
7283
7384
// Clear the existing PointPrimitiveCollection
7485
content.points.removeAll();
@@ -189,8 +200,8 @@ async function getGeoRecord(pid) {
189200
return "unset";
190201
}
191202
const q = `SELECT row_id, pid, otype, latitude, longitude FROM nodes WHERE otype='GeospatialCoordLocation' AND pid=?`;
192-
const result = await db.queryRow(q, [pid]);
193-
return result;
203+
const rows = await loadData(q, [pid], "loading_geo", "geo");
204+
return rows && rows.length ? rows[0] : null;
194205
}
195206
196207
async function get_samples_1(pid) {
@@ -215,8 +226,8 @@ async function get_samples_1(pid) {
215226
AND g.otype = 'GeospatialCoordLocation'
216227
AND g.pid = ?
217228
`;
218-
const result = await db.query(q, [pid]);
219-
return result;
229+
const result = await loadData(q, [pid], "loading_s1", "samples_1");
230+
return result ?? [];
220231
}
221232
222233
async function get_samples_2(pid) {
@@ -245,8 +256,8 @@ async function get_samples_2(pid) {
245256
AND g.otype = 'GeospatialCoordLocation'
246257
AND g.pid = ?
247258
`;
248-
const result = await db.query(q, [pid]);
249-
return result;
259+
const result = await loadData(q, [pid], "loading_s2", "samples_2");
260+
return result ?? [];
250261
}
251262
252263
async function locationUsedBy(rowid){
@@ -258,7 +269,38 @@ async function locationUsedBy(rowid){
258269
}
259270
260271
mutable clickedPointId = "unset";
261-
selectedGeoRecord = await getGeoRecord(clickedPointId);
272+
// Loading flags to control UI clearing while fetching
273+
mutable geoLoading = false;
274+
mutable s1Loading = false;
275+
mutable s2Loading = false;
276+
277+
// Precompute selection-driven data with loading flags
278+
selectedGeoRecord = {
279+
mutable geoLoading = true;
280+
try {
281+
return await getGeoRecord(clickedPointId);
282+
} finally {
283+
mutable geoLoading = false;
284+
}
285+
}
286+
287+
selectedSamples1 = {
288+
mutable s1Loading = true;
289+
try {
290+
return await get_samples_1(clickedPointId);
291+
} finally {
292+
mutable s1Loading = false;
293+
}
294+
}
295+
296+
selectedSamples2 = {
297+
mutable s2Loading = true;
298+
try {
299+
return await get_samples_2(clickedPointId);
300+
} finally {
301+
mutable s2Loading = false;
302+
}
303+
}
262304
263305
md`Retrieved ${pointdata.length} locations from ${parquet_path}.`;
264306
```
@@ -293,49 +335,22 @@ viewof pointdata = {
293335

294336
The click point ID is "${clickedPointId}".
295337

338+
<div id="loading_geo" hidden>Loading selected location…</div>
339+
296340
```{ojs}
297341
//| echo: false
298-
md`\`\`\`
342+
geoLoading ? md`(loading…)` : md`\`\`\`
299343
${JSON.stringify(selectedGeoRecord, null, 2)}
300344
\`\`\`
301345
`
302346
```
303347

304-
## Sample Data
305-
306-
First 10 rows of the dataset to understand the data structure:
307-
308-
```{ojs}
309-
//| code-fold: true
310-
sampleData = {
311-
const query = `SELECT * FROM nodes LIMIT 10`;
312-
const data = await loadData(query, [], "loading_sample");
313-
return data;
314-
}
315-
```
316-
317-
<div id="loading_sample">Loading sample data...</div>
318-
319-
```{ojs}
320-
//| code-fold: true
321-
viewof sampleTable = {
322-
const data_table = Inputs.table(sampleData, {
323-
layout: "auto",
324-
width: {
325-
pid: 200,
326-
otype: 150
327-
}
328-
});
329-
return data_table;
330-
}
331-
```
332-
333348
## getGeoRecord (selected)
334349

335350
```{ojs}
336351
//| code-fold: true
337352
pid = clickedPointId
338-
testrecord = await getGeoRecord(pid);
353+
testrecord = selectedGeoRecord;
339354
```
340355

341356
```{ojs}
@@ -348,10 +363,17 @@ ${JSON.stringify(testrecord, null, 2)}
348363

349364
## Related Sample Path 1 (selected)
350365

366+
<div id="loading_s1" hidden>Loading related samples (path 1)…</div>
367+
368+
Path 1 (direct_event_location): find MaterialSampleRecord items whose producing SamplingEvent has a direct sample_location pointing to the clicked GeospatialCoordLocation (pid).
369+
370+
- Chain: MaterialSampleRecord → produced_by → SamplingEvent → sample_location → GeospatialCoordLocation (clicked pid)
371+
- This matches the "direct_samples" concept in the Python notebook and is labeled as `location_path = 'direct_event_location'` in the query.
372+
351373
```{ojs}
352374
//| echo: false
353-
samples_1 = await get_samples_1(clickedPointId)
354-
md`\`\`\`
375+
samples_1 = selectedSamples1
376+
s1Loading ? md`(loading…)` : md`\`\`\`
355377
${JSON.stringify(samples_1, null, 2)}
356378
\`\`\`
357379
`
@@ -360,10 +382,17 @@ ${JSON.stringify(samples_1, null, 2)}
360382

361383
## Related Sample Path 2 (selected)
362384

385+
<div id="loading_s2" hidden>Loading related samples (path 2)…</div>
386+
387+
Path 2 (via_site_location): find MaterialSampleRecord items whose producing SamplingEvent references a SamplingSite, and that site’s site_location points to the clicked GeospatialCoordLocation (pid).
388+
389+
- Chain: MaterialSampleRecord → produced_by → SamplingEvent → sampling_site → SamplingSite → site_location → GeospatialCoordLocation (clicked pid)
390+
- This matches the "samples_via_sites" concept in the Python notebook and is labeled as `location_path = 'via_site_location'` in the query.
391+
363392
```{ojs}
364393
//| echo: false
365-
samples_2 = await get_samples_2(clickedPointId)
366-
md`\`\`\`
394+
samples_2 = selectedSamples2
395+
s2Loading ? md`(loading…)` : md`\`\`\`
367396
${JSON.stringify(samples_2, null, 2)}
368397
\`\`\`
369398
`

0 commit comments

Comments
 (0)