Skip to content

Commit a3cff4c

Browse files
Copilotlstein
andcommitted
bugfix(frontend): Make spinner appear after clicking on umap point
Make colorizeUmap async to show spinner before heavy Plotly operations Co-authored-by: lstein <[email protected]> Await Plotly operations to keep spinner visible during rendering Co-authored-by: lstein <[email protected]> Move spinner inside umapContent to fix positioning Co-authored-by: lstein <[email protected]> Hide spinner in searchResultsChanged handler instead of click handlers Co-authored-by: lstein <[email protected]>
1 parent f939631 commit a3cff4c

File tree

2 files changed

+62
-64
lines changed

2 files changed

+62
-64
lines changed

photomap/frontend/static/javascript/umap.js

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,14 @@ export async function fetchUmapData() {
238238
scrollZoom: true,
239239
};
240240

241-
Plotly.newPlot("umapPlot", [allPointsTrace, currentImageTrace], layout, config).then((gd) => {
241+
Plotly.newPlot("umapPlot", [allPointsTrace, currentImageTrace], layout, config).then(async (gd) => {
242242
document.getElementById("umapContent").style.display = "block";
243243
setUmapWindowSize("fullscreen");
244244
hideUmapSpinner();
245245

246246
window.dispatchEvent(new CustomEvent("umapRedrawn"));
247247

248-
setUmapColorMode();
248+
await setUmapColorMode();
249249
let hoverTimer = null;
250250
let isHovering = false;
251251

@@ -410,7 +410,7 @@ export async function fetchUmapData() {
410410
// Dispatch event to notify that UMAP data has been loaded
411411
window.dispatchEvent(new CustomEvent("umapDataLoaded"));
412412

413-
setUmapColorMode();
413+
await setUmapColorMode();
414414
} finally {
415415
hideUmapSpinner();
416416
}
@@ -457,7 +457,7 @@ plotDiv.addEventListener("mouseleave", () => {
457457
});
458458

459459
// --- Dynamic Colorization ---
460-
export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
460+
export async function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
461461
if (!points.length) {
462462
return;
463463
}
@@ -467,6 +467,9 @@ export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
467467
return;
468468
}
469469

470+
// Yield to the browser to allow spinner to render before heavy Plotly operations
471+
await new Promise((resolve) => setTimeout(resolve, 0));
472+
470473
if (highlight && searchResults.length > 0) {
471474
const searchSet = new Set(searchResults.map((r) => r.index));
472475

@@ -475,7 +478,7 @@ export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
475478
const highlightedPoints = points.filter((p) => searchSet.has(p.index));
476479

477480
// Update main trace with only regular points
478-
Plotly.restyle(
481+
await Plotly.restyle(
479482
"umapPlot",
480483
{
481484
x: [regularPoints.map((p) => p.x)],
@@ -508,9 +511,9 @@ export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
508511
};
509512

510513
if (highlightTraceIdx === -1) {
511-
Plotly.addTraces(plotDiv, [highlightTrace]);
514+
await Plotly.addTraces(plotDiv, [highlightTrace]);
512515
} else {
513-
Plotly.restyle(
516+
await Plotly.restyle(
514517
plotDiv,
515518
{
516519
x: [highlightTrace.x],
@@ -527,21 +530,21 @@ export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
527530
// Ensure Current Image marker stays on top
528531
const markerTraceIndex = plotDiv.data.findIndex((trace) => trace.name === "Current Image");
529532
if (markerTraceIndex !== -1 && markerTraceIndex !== plotDiv.data.length - 1) {
530-
Plotly.moveTraces(plotDiv, markerTraceIndex, plotDiv.data.length - 1);
533+
await Plotly.moveTraces(plotDiv, markerTraceIndex, plotDiv.data.length - 1);
531534
}
532535
} else {
533536
// Remove highlight trace if it exists
534537
const highlightTraceIdx = plotDiv.data?.findIndex((t) => t.name === "HighlightedPoints");
535538
if (highlightTraceIdx !== -1) {
536-
Plotly.deleteTraces(plotDiv, highlightTraceIdx);
539+
await Plotly.deleteTraces(plotDiv, highlightTraceIdx);
537540
}
538541

539542
// Restore ALL points to main trace with normal coloring
540543
const markerColors = points.map((p) => getClusterColor(p.cluster));
541544
const markerAlphas = points.map((p) => (p.cluster === -1 ? 0.2 : 0.75));
542545
const markerSizes = points.map(() => 5);
543546

544-
Plotly.restyle(
547+
await Plotly.restyle(
545548
"umapPlot",
546549
{
547550
x: [points.map((p) => p.x)],
@@ -558,7 +561,7 @@ export function colorizeUmap({ highlight = false, searchResults = [] } = {}) {
558561
// Ensure Current Image marker stays on top after removing highlight
559562
const markerTraceIndex = plotDiv.data.findIndex((trace) => trace.name === "Current Image");
560563
if (markerTraceIndex !== -1 && markerTraceIndex !== plotDiv.data.length - 1) {
561-
Plotly.moveTraces(plotDiv, markerTraceIndex, plotDiv.data.length - 1);
564+
await Plotly.moveTraces(plotDiv, markerTraceIndex, plotDiv.data.length - 1);
562565
}
563566
}
564567
}
@@ -569,8 +572,8 @@ window.addEventListener("stateReady", () => {
569572
const highlightCheckbox = document.getElementById("umapHighlightSelection");
570573
if (highlightCheckbox) {
571574
highlightCheckbox.checked = false;
572-
highlightCheckbox.addEventListener("change", () => {
573-
setUmapColorMode();
575+
highlightCheckbox.addEventListener("change", async () => {
576+
await setUmapColorMode();
574577
});
575578
}
576579

@@ -661,9 +664,11 @@ function updateExitFullscreenCheckboxState() {
661664
}
662665

663666
// --- Update colorization after search or cluster selection ---
664-
window.addEventListener("searchResultsChanged", (e) => {
667+
window.addEventListener("searchResultsChanged", async (e) => {
665668
updateUmapColorModeAvailability(e.detail.results);
666-
setUmapColorMode();
669+
await setUmapColorMode();
670+
// Hide spinner after colorization completes
671+
hideUmapSpinner();
667672
// deactivate fullscreen mode when search results have come in (if enabled)
668673
if (state.searchResults.length > 0 && isFullscreen && state.umapExitFullscreenOnSelection) {
669674
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
@@ -920,8 +925,8 @@ function removeUmapThumbnail() {
920925
umapThumbnailDiv = null;
921926
}
922927

923-
export function setUmapColorMode() {
924-
colorizeUmap({
928+
export async function setUmapColorMode() {
929+
await colorizeUmap({
925930
highlight: document.getElementById("umapHighlightSelection")?.checked,
926931
searchResults: state.searchResults,
927932
});
@@ -943,7 +948,7 @@ function updateUmapColorModeAvailability(searchResults = []) {
943948
highlightCheckbox.disabled = true;
944949
highlightCheckbox.parentElement.style.opacity = "0.5";
945950
}
946-
setUmapColorMode();
951+
// Note: setUmapColorMode is called by the searchResultsChanged event handler
947952
}
948953

949954
// ------------- Handling Landmark Thumbnails -------------
@@ -1265,29 +1270,25 @@ async function handleClusterClick(clickedIndex) {
12651270
// Yield to the browser to allow spinner to render before heavy computation
12661271
await new Promise((resolve) => setTimeout(resolve, 0));
12671272

1268-
try {
1269-
const clickedCluster = clickedPoint.cluster;
1270-
const clusterColor = getClusterColor(clickedCluster);
1271-
let clusterIndices = points.filter((p) => p.cluster === clickedCluster).map((p) => p.index);
1273+
const clickedCluster = clickedPoint.cluster;
1274+
const clusterColor = getClusterColor(clickedCluster);
1275+
let clusterIndices = points.filter((p) => p.cluster === clickedCluster).map((p) => p.index);
12721276

1273-
// Remove clickedFilename from the list
1274-
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
1277+
// Remove clickedFilename from the list
1278+
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
12751279

1276-
// --- Greedy random walk order from clicked point ---
1277-
const sort_algorithm = clusterIndices.length > randomWalkMaxSize ? proximityClusterOrder : randomWalkClusterOrder;
1278-
const sortedClusterIndices = sort_algorithm([clickedIndex, ...clusterIndices], points, clickedIndex);
1280+
// --- Greedy random walk order from clicked point ---
1281+
const sort_algorithm = clusterIndices.length > randomWalkMaxSize ? proximityClusterOrder : randomWalkClusterOrder;
1282+
const sortedClusterIndices = sort_algorithm([clickedIndex, ...clusterIndices], points, clickedIndex);
12791283

1280-
const clusterMembers = sortedClusterIndices.map((index) => ({
1281-
index: index,
1282-
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1283-
color: clusterColor,
1284-
}));
1284+
const clusterMembers = sortedClusterIndices.map((index) => ({
1285+
index: index,
1286+
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1287+
color: clusterColor,
1288+
}));
12851289

1286-
setSearchResults(clusterMembers, "cluster");
1287-
} finally {
1288-
// Always hide spinner, even if there's an error
1289-
hideUmapSpinner();
1290-
}
1290+
setSearchResults(clusterMembers, "cluster");
1291+
// Note: spinner is hidden by searchResultsChanged event handler after colorization completes
12911292
}
12921293

12931294
// Handle single image selection (navigate to clicked image)
@@ -1303,21 +1304,17 @@ async function handleImageClick(clickedIndex) {
13031304
// Yield to the browser to allow spinner to render before heavy computation
13041305
await new Promise((resolve) => setTimeout(resolve, 0));
13051306

1306-
try {
1307-
// Clear any existing search selection
1308-
exitSearchMode();
1307+
// Clear any existing search selection
1308+
exitSearchMode();
13091309

1310-
// Navigate directly to the clicked image without entering search mode
1311-
slideState.navigateToIndex(clickedIndex, false);
1310+
// Navigate directly to the clicked image without entering search mode
1311+
slideState.navigateToIndex(clickedIndex, false);
13121312

1313-
// Exit fullscreen mode if enabled
1314-
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1315-
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
1316-
}
1317-
} finally {
1318-
// Always hide spinner, even if there's an error
1319-
hideUmapSpinner();
1313+
// Exit fullscreen mode if enabled
1314+
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1315+
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
13201316
}
1317+
// Note: spinner is hidden by searchResultsChanged event handler after colorization completes
13211318
}
13221319

13231320
// -------------------- Window Management --------------------

photomap/frontend/templates/modules/umap-floating-window.html

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,24 +108,25 @@
108108
</button>
109109
</div>
110110
</div>
111-
<!-- Spinner (always visible during loading) -->
112-
<div
113-
id="umapSpinner"
114-
style="
115-
display: none;
116-
position: absolute;
117-
left: 50%;
118-
top: 50%;
119-
transform: translate(-50%, -50%);
120-
z-index: 10000;
121-
pointer-events: none;
122-
"
123-
>
124-
<div class="umap-spinner"></div>
125-
</div>
126111

127112
<!-- All other content hidden until plot is ready -->
128113
<div id="umapContent" style="display: none">
114+
<!-- Spinner (always visible during loading) -->
115+
<div
116+
id="umapSpinner"
117+
style="
118+
display: none;
119+
position: absolute;
120+
left: 50%;
121+
top: 50%;
122+
transform: translate(-50%, -50%);
123+
z-index: 10000;
124+
pointer-events: none;
125+
"
126+
>
127+
<div class="umap-spinner"></div>
128+
</div>
129+
129130
<!-- Plotly Plot -->
130131
<div id="umapPlot"></div>
131132

0 commit comments

Comments
 (0)