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
This commit is contained in:
Chris Coutinho
2025-12-16 00:05:48 +01:00
parent b246a03ac4
commit fba4b9b785
+57
View File
@@ -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)
},
},
}
</script>
@@ -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