Skip to content

Commit 1f34ec2

Browse files
authored
Highlight selected scatter point (#645)
1 parent bfe7755 commit 1f34ec2

2 files changed

Lines changed: 102 additions & 2 deletions

File tree

ml_peg/app/utils/build_callbacks.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@
1010
from pathlib import Path
1111
from 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+
)
1422
from dash.dcc import Graph
1523
from dash.development.base_component import Component
1624
from 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+
169243
def 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"),

ml_peg/app/utils/weas.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ def generate_weas_html(
3838
frame = index
3939
atoms_txt = "atoms"
4040

41+
# In traj mode, report the current frame to the parent page on every change
42+
# (play, step, slider) so a linked plot can highlight the matching point.
43+
# WEAS has no frame-change event, so poll editor.avr.currentFrame via rAF.
44+
frame_reporter = (
45+
"""
46+
let __mlPegLastFrame = editor.avr.currentFrame;
47+
function __mlPegReportFrame() {
48+
const f = editor.avr.currentFrame;
49+
if (f !== __mlPegLastFrame) {
50+
__mlPegLastFrame = f;
51+
window.parent.postMessage(
52+
{type: "ml-peg-weas-frame", frame: f}, "*"
53+
);
54+
}
55+
requestAnimationFrame(__mlPegReportFrame);
56+
}
57+
requestAnimationFrame(__mlPegReportFrame);
58+
"""
59+
if mode == "traj"
60+
else ""
61+
)
62+
4163
return f"""
4264
<!doctype html>
4365
<html lang="en">
@@ -116,7 +138,7 @@ def generate_weas_html(
116138
117139
editor.avr.currentFrame = {frame};
118140
editor.render();
119-
141+
{frame_reporter}
120142
</script>
121143
</body>
122144
</html>

0 commit comments

Comments
 (0)