Skip to content

Commit cffc898

Browse files
Copilotlstein
andauthored
bugfix(frontend): Make spinner appear after clicking on umap point (#204)
Make colorizeUmap async to show spinner before heavy Plotly operations Await Plotly operations to keep spinner visible during rendering Move spinner inside umapContent to fix positioning Hide spinner in searchResultsChanged handler instead of click handlers Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
1 parent f939631 commit cffc898

2 files changed

Lines changed: 62 additions & 64 deletions

File tree

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)