1010from pathlib import Path
1111from typing import Literal
1212
13- from dash import Input , Output , State , callback , callback_context , html
13+ from dash import (
14+ Input ,
15+ Output ,
16+ State ,
17+ callback ,
18+ callback_context ,
19+ clientside_callback ,
20+ html ,
21+ )
1422from dash .dcc import Graph
1523from dash .development .base_component import Component
1624from dash .exceptions import PreventUpdate
@@ -166,6 +174,72 @@ def show_plot(active_cell, current_table_data) -> Div:
166174 return Div (TABLE_HINT , style = INSTRUCTION_STYLE ), None
167175
168176
177+ _HIGHLIGHTED_SCATTERS : set [str ] = set ()
178+
179+
180+ def _register_point_highlight (scatter_id : str ) -> None :
181+ """
182+ Ring the most recently clicked point of a scatter plot.
183+
184+ Registered once per ``scatter_id`` and runs entirely client-side, so it adds
185+ no server load and works for any scatter wired for click interactions. On
186+ each click a single ring marker (a transparent-fill ``__clicked_point__``
187+ trace) replaces the previous one. Its type and axes are copied from the
188+ clicked trace so the ring sits on the same render layer (svg vs WebGL) and
189+ subplot as the point.
190+
191+ Parameters
192+ ----------
193+ scatter_id
194+ ID of the Dash ``Graph`` whose clicked point should be highlighted.
195+ """
196+ if scatter_id in _HIGHLIGHTED_SCATTERS :
197+ return
198+ _HIGHLIGHTED_SCATTERS .add (scatter_id )
199+
200+ clientside_callback (
201+ """
202+ function(clickData, figure) {
203+ const dc = window.dash_clientside;
204+ if (!clickData || !figure || !figure.data) { return dc.no_update; }
205+ const pt = clickData.points[0];
206+ const src = figure.data[pt.curveNumber] || {};
207+ const data = figure.data.filter(
208+ (t) => t.name !== '__clicked_point__'
209+ );
210+ data.push({
211+ x: [pt.x],
212+ y: [pt.y],
213+ type: src.type || 'scatter',
214+ mode: 'markers',
215+ name: '__clicked_point__',
216+ xaxis: src.xaxis,
217+ yaxis: src.yaxis,
218+ hoverinfo: 'skip',
219+ showlegend: false,
220+ marker: {
221+ size: 16,
222+ color: 'rgba(0,0,0,0)',
223+ line: {color: '#ff1493', width: 3},
224+ },
225+ });
226+ // Record the clicked curve so a playing WEAS trajectory can move
227+ // this ring to the matching frame's point (weas_frame_follow.js).
228+ window.__mlPegActiveTraj = {
229+ scatterId: "__SCATTER_ID__",
230+ x: src.x || [],
231+ y: src.y || [],
232+ };
233+ return Object.assign({}, figure, {data: data});
234+ }
235+ """ .replace ("__SCATTER_ID__" , scatter_id ),
236+ Output (scatter_id , "figure" , allow_duplicate = True ),
237+ Input (scatter_id , "clickData" ),
238+ State (scatter_id , "figure" ),
239+ prevent_initial_call = True ,
240+ )
241+
242+
169243def plot_from_scatter (
170244 scatter_id : str ,
171245 plot_id : str ,
@@ -184,6 +258,7 @@ def plot_from_scatter(
184258 List of plots to show, in same order as scatter data.
185259 """
186260 register_plot_download_callbacks ()
261+ _register_point_highlight (scatter_id )
187262
188263 @callback (
189264 Output (plot_id , "children" , allow_duplicate = True ),
@@ -234,6 +309,7 @@ def struct_from_scatter(
234309 Whether to display a single structure ("struct"), or trajectory from an initial
235310 image ("traj"). Default is "struct".
236311 """
312+ _register_point_highlight (scatter_id )
237313
238314 @callback (
239315 Output (struct_id , "children" , allow_duplicate = True ),
@@ -317,6 +393,7 @@ def struct_from_multi_scatters(
317393 When the `i`th data point of the `j`th curve of "test-figure" is clicked,
318394 `structs[j][i]` will be rendered in the "test-placeholder" Div.
319395 """
396+ _register_point_highlight (scatter_id )
320397
321398 @callback (
322399 Output (struct_id , "children" , allow_duplicate = True ),
@@ -918,6 +995,7 @@ def model_asset_from_scatter(
918995 missing_message
919996 Message shown when no asset can be produced for the click event.
920997 """
998+ _register_point_highlight (scatter_id )
921999
9221000 @callback (
9231001 Output (asset_container_id , "children" ),
0 commit comments