From fba4b9b78599aa75ca642817f99efba13222862b Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 16 Dec 2025 00:05:48 +0100 Subject: [PATCH] feat: add click interactivity to Plotly 3D scatter chart Enable users to click on points in the vector space visualization to open the chunk viewer modal, providing a more direct interaction method alongside the existing "Show Chunk" button. Implementation details: - Register plotly_click event handler in renderPlot() after chart creation - Add handlePlotClick() method to process click events - Use point index mapping to access full result object from this.results - Add loading guard in viewChunk() to prevent concurrent chunk loading - Add cursor styling: pointer for result points, default for query point - Add beforeDestroy() lifecycle hook to cleanup event handlers Features: - Both interaction methods work: click chart points OR "Show Chunk" button - Only result points (trace 0) are clickable, query point (trace 1) ignored - Pointer cursor on hover indicates clickable points - Loading state prevents rapid clicks from causing issues - Memory leak prevention through proper event handler cleanup Technical approach: - Uses index mapping (not data duplication) for efficiency - Results and coordinates arrays have guaranteed 1:1 mapping from API - Event handler re-registered on each chart re-render - CSS-based cursor styling (more performant than JS hover handlers) Testing: - ESLint validation passed - Follows Vue 2.7 component property order conventions - Compatible with existing chunk viewer modal --- third_party/astroglobe/src/App.vue | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/third_party/astroglobe/src/App.vue b/third_party/astroglobe/src/App.vue index 0e68238..b656c33 100644 --- a/third_party/astroglobe/src/App.vue +++ b/third_party/astroglobe/src/App.vue @@ -490,6 +490,13 @@ export default { return this.results.filter(r => (r.score || 0) >= threshold) }, }, + beforeDestroy() { + // Clean up Plotly event handlers to prevent memory leaks + const plotDiv = document.getElementById('viz-plot') + if (plotDiv && plotDiv.on) { + plotDiv.removeAllListeners('plotly_click') + } + }, methods: { toggleDocType(docTypeId, checked) { if (checked && !this.selectedDocTypes.includes(docTypeId)) { @@ -733,6 +740,12 @@ export default { } Plotly.newPlot('viz-plot', traces, layout, config) + + // Register click event handler for result points + const plotDiv = document.getElementById('viz-plot') + if (plotDiv) { + plotDiv.on('plotly_click', this.handlePlotClick) + } }, updatePlot() { @@ -751,6 +764,11 @@ export default { }, async viewChunk(result) { + // Guard against concurrent loading + if (this.viewerLoading) { + return + } + this.showViewer = true this.viewerLoading = true this.viewerTitle = result.title || 'Chunk Viewer' @@ -828,6 +846,35 @@ export default { this.showViewer = false this.pdfTotalPages = 0 }, + + handlePlotClick(eventData) { + // Only handle clicks on trace 0 (document results) + // Trace 1 is the query point - ignore clicks on it + if (!eventData.points || eventData.points.length === 0) { + return + } + + const point = eventData.points[0] + const traceIndex = point.curveNumber // 0 = documents, 1 = query + const pointIndex = point.pointNumber // Index in trace data + + // Ignore clicks on query point (trace 1) + if (traceIndex !== 0) { + return + } + + // Access full result object using pointIndex + // Results array is 1:1 with coordinates array (guaranteed by API) + const result = this.results[pointIndex] + + if (!result) { + console.warn('Click handler: result not found for index', pointIndex) + return + } + + // Call existing viewChunk method + this.viewChunk(result) + }, }, } @@ -955,6 +1002,16 @@ export default { #viz-plot { width: 100%; height: 100%; + + // Pointer cursor for clickable result points (trace 0) + :deep(.scatterlayer .trace:first-child .point) { + cursor: pointer !important; + } + + // Default cursor for query point (trace 1) + :deep(.scatterlayer .trace:nth-child(2) .point) { + cursor: default !important; + } } // Results