Skip to content

Commit ac00ff4

Browse files
Copilotlstein
andcommitted
Add spinner feedback when clicking UMAP clusters
Show the UMAP spinner immediately when a user clicks on a cluster or image in the semantic map, providing visual feedback during processing. The spinner is hidden after the cluster selection and UI updates complete. - Added showUmapSpinner() call at the start of handleClusterClick() - Added showUmapSpinner() call at the start of handleImageClick() - Wrapped operations in try/finally to ensure spinner is always hidden - Added requestAnimationFrame delay to ensure UI updates before hiding spinner This improves UX on large photo collections where cluster selection can be slow. Co-authored-by: lstein <111189+lstein@users.noreply.github.com> Add setTimeout(0) to allow spinner to render before heavy computation The issue was that the synchronous operations were executing immediately after showing the spinner, without giving the browser a chance to paint the DOM changes. By adding `await new Promise(resolve => setTimeout(resolve, 0))`, we yield control back to the event loop, allowing the browser to render the spinner before the expensive cluster sorting operations begin. This ensures the spinner is visible during the 1-3 second delay on large clusters. Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
1 parent 3f698b1 commit ac00ff4

File tree

3 files changed

+60
-36
lines changed

3 files changed

+60
-36
lines changed

photomap/frontend/static/css/spinners.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
left: 50%;
1717
top: 50%;
1818
transform: translate(-50%, -50%);
19-
z-index: 1000;
19+
z-index: 10000;
20+
pointer-events: none;
2021
}
2122

2223
#umapSpinner .umap-spinner {

photomap/frontend/static/javascript/umap.js

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,50 +1256,72 @@ function proximityClusterOrder(clusterIndices, points, startIndex) {
12561256
async function handleClusterClick(clickedIndex) {
12571257
const clickedPoint = points.find((p) => p.index === clickedIndex);
12581258
if (!clickedPoint) return;
1259+
1260+
// Show spinner immediately to provide visual feedback
1261+
showUmapSpinner();
1262+
1263+
// Yield to the browser to allow spinner to render before heavy computation
1264+
await new Promise(resolve => setTimeout(resolve, 0));
1265+
1266+
try {
1267+
const clickedCluster = clickedPoint.cluster;
1268+
const clusterColor = getClusterColor(clickedCluster);
1269+
let clusterIndices = points
1270+
.filter((p) => p.cluster === clickedCluster)
1271+
.map((p) => p.index);
1272+
1273+
// Remove clickedFilename from the list
1274+
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
1275+
1276+
// --- Greedy random walk order from clicked point ---
1277+
const sort_algorithm =
1278+
clusterIndices.length > randomWalkMaxSize
1279+
? proximityClusterOrder
1280+
: randomWalkClusterOrder;
1281+
const sortedClusterIndices = sort_algorithm(
1282+
[clickedIndex, ...clusterIndices],
1283+
points,
1284+
clickedIndex
1285+
);
12591286

1260-
const clickedCluster = clickedPoint.cluster;
1261-
const clusterColor = getClusterColor(clickedCluster);
1262-
let clusterIndices = points
1263-
.filter((p) => p.cluster === clickedCluster)
1264-
.map((p) => p.index);
1265-
1266-
// Remove clickedFilename from the list
1267-
clusterIndices = clusterIndices.filter((fn) => fn !== clickedIndex);
1268-
1269-
// --- Greedy random walk order from clicked point ---
1270-
const sort_algorithm =
1271-
clusterIndices.length > randomWalkMaxSize
1272-
? proximityClusterOrder
1273-
: randomWalkClusterOrder;
1274-
const sortedClusterIndices = sort_algorithm(
1275-
[clickedIndex, ...clusterIndices],
1276-
points,
1277-
clickedIndex
1278-
);
1279-
1280-
const clusterMembers = sortedClusterIndices.map((index) => ({
1281-
index: index,
1282-
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1283-
color: clusterColor,
1284-
}));
1287+
const clusterMembers = sortedClusterIndices.map((index) => ({
1288+
index: index,
1289+
cluster: clickedCluster === -1 ? "unclustered" : clickedCluster,
1290+
color: clusterColor,
1291+
}));
12851292

1286-
setSearchResults(clusterMembers, "cluster");
1293+
setSearchResults(clusterMembers, "cluster");
1294+
} finally {
1295+
// Always hide spinner, even if there's an error
1296+
hideUmapSpinner();
1297+
}
12871298
}
12881299

12891300
// Handle single image selection (navigate to clicked image)
12901301
async function handleImageClick(clickedIndex) {
12911302
const clickedPoint = points.find((p) => p.index === clickedIndex);
12921303
if (!clickedPoint) return;
1293-
1294-
// Clear any existing search selection
1295-
exitSearchMode();
12961304

1297-
// Navigate directly to the clicked image without entering search mode
1298-
slideState.navigateToIndex(clickedIndex, false);
1305+
// Show spinner immediately to provide visual feedback
1306+
showUmapSpinner();
1307+
1308+
// Yield to the browser to allow spinner to render before heavy computation
1309+
await new Promise(resolve => setTimeout(resolve, 0));
12991310

1300-
// Exit fullscreen mode if enabled
1301-
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1302-
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
1311+
try {
1312+
// Clear any existing search selection
1313+
exitSearchMode();
1314+
1315+
// Navigate directly to the clicked image without entering search mode
1316+
slideState.navigateToIndex(clickedIndex, false);
1317+
1318+
// Exit fullscreen mode if enabled
1319+
if (isFullscreen && state.umapExitFullscreenOnSelection) {
1320+
setTimeout(() => toggleFullscreen(false), 100); // slight delay to avoid flicker
1321+
}
1322+
} finally {
1323+
// Always hide spinner, even if there's an error
1324+
hideUmapSpinner();
13031325
}
13041326
}
13051327

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@
117117
left: 50%;
118118
top: 50%;
119119
transform: translate(-50%, -50%);
120-
z-index: 1000;
120+
z-index: 10000;
121+
pointer-events: none;
121122
"
122123
>
123124
<div class="umap-spinner"></div>

0 commit comments

Comments
 (0)