Skip to content

Commit 7783d92

Browse files
committed
update spectrum viewer
1 parent cb15f99 commit 7783d92

File tree

1 file changed

+83
-125
lines changed

1 file changed

+83
-125
lines changed

examples/serve/panels-demo/demo_panels/panel_spectrum.py

Lines changed: 83 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import math
2-
import json
32
from typing import Any
43

54
import altair as alt
@@ -14,7 +13,6 @@
1413
from chartlets import Component, Input, State, Output
1514
from chartlets.components import (
1615
Box,
17-
Button,
1816
Typography,
1917
VegaChart,
2018
Radio,
@@ -29,6 +27,8 @@
2927

3028
panel = Panel(__name__, title="Spectrum View (Demo)", icon="light", position=4)
3129

30+
THROTTLE_TOTAL_SPECTRUM_PLOTS = 10
31+
3232

3333
@panel.layout(
3434
State("@app", "selectedDatasetId"),
@@ -56,53 +56,24 @@ def render_panel(
5656
text = f"{dataset_id}"
5757
place_text = Typography(id="text", children=[text], align="left")
5858

59-
active_radio = Radio(id="active_radio", value="active", label="Active Mode")
60-
save_radio = Radio(id="save_radio", value="save", label="Save Mode")
59+
update_radio = Radio(id="update_radio", value="update", label="Update")
60+
add_radio = Radio(id="add_radio", value="add", label="Add")
6161

6262
exploration_radio_group = RadioGroup(
6363
id="exploration_radio_group",
64-
children=[active_radio, save_radio],
64+
children=[update_radio, add_radio],
6565
label="Exploration Mode",
66-
)
67-
68-
# Ideas
69-
# 1. Adding radio-button for two modes:
70-
# update mode - reactive to changes to dataset/places/time/variables
71-
# active mode -
72-
73-
# How should spectrum viewer behave?
74-
# It should be reactive to changes to dataset/places/times
75-
# How to freeze the current spectrum?
76-
# We add a button to add it permanently to the graph and then when a new point is
77-
# selected it becomes reactive again.
78-
79-
# Second mode - Just change for time but new line in graph for a new point
80-
81-
# First version: Reactive to time and place changes
82-
# Add button: Would add the spectrum view of current time and place to the graph
83-
# with legend place/time (static)
84-
# Delete button: Would delete the last one the spectrum views in the plot
85-
# Move the text align to left
86-
87-
# Make line chart and bar chart
88-
89-
delete_button = Button(
90-
id="delete_button", text="Delete last point", style={"maxWidth": 100}
91-
)
92-
93-
controls = Box(
94-
children=[exploration_radio_group, delete_button],
9566
style={
9667
"display": "flex",
9768
"flexDirection": "row",
98-
"alignItems": "center",
99-
"gap": 6,
100-
"padding": 6,
10169
},
70+
tooltip="'Update': Clear the chart but the current selection if any. "
71+
"'Add': Current spectrum is added and new point selections will be "
72+
"added as new spectra",
10273
)
10374

10475
control_bar = Box(
105-
children=[place_text, controls],
76+
children=[place_text, exploration_radio_group],
10677
style={
10778
"display": "flex",
10879
"flexDirection": "row",
@@ -117,24 +88,23 @@ def render_panel(
11788
id="error_message", style={"color": "red"}, children=[""]
11889
)
11990

120-
places_stack_storage = Typography(
121-
id="places_stack_storage",
122-
children=["[]"],
123-
style={"display": "none"},
91+
note = Typography(
92+
id="note",
93+
children=["NOTE: You can only add a maximum of 10 spectrum plots at a time"],
12494
)
12595

12696
return Box(
12797
children=[
12898
"Choose an exploration mode and create/select points to view the Spectrum data.",
99+
note,
129100
control_bar,
130101
error_message,
131102
plot,
132-
places_stack_storage,
133103
],
134104
style={
135105
"display": "flex",
136106
"flexDirection": "column",
137-
"alignItems": "center",
107+
"alignItems": "left",
138108
"width": "100%",
139109
"height": "100%",
140110
"gap": 6,
@@ -220,12 +190,14 @@ def update_text(
220190
Input("@app", "selectedTimeLabel"),
221191
Input("@app", "selectedPlaceGeometry"),
222192
State("@app", "selectedPlaceGroup"),
223-
State("exploration_radio_group", "value"),
193+
Input("exploration_radio_group", "value"),
224194
State("plot", "chart"),
225-
State("places_stack_storage", "children"),
195+
State("@container", "spectrum_list"),
196+
State("@container", "previous_mode"),
226197
Output("plot", "chart"),
227198
Output("error_message", "children"),
228-
Output("places_stack_storage", "children"),
199+
Output("@container", "spectrum_list"),
200+
Output("@container", "previous_mode"),
229201
)
230202
def update_plot(
231203
ctx: Context,
@@ -235,19 +207,12 @@ def update_plot(
235207
place_group: list[dict[str, Any]] | None = None,
236208
exploration_radio_group: str | None = None,
237209
current_chart: alt.Chart | None = None,
238-
places_stack_json: list | None = None,
239-
) -> tuple[alt.Chart | None, str, list]:
240-
import json
241-
242-
places_stack = []
243-
if places_stack_json and len(places_stack_json) > 0:
244-
try:
245-
places_stack = json.loads(places_stack_json[0])
246-
except (json.JSONDecodeError, IndexError):
247-
places_stack = []
248-
210+
spectrum_list: list[str] | None = None,
211+
previous_mode: str | None = None,
212+
) -> tuple[alt.Chart | None, str, list, str]:
213+
print("spectrum list", spectrum_list)
249214
if exploration_radio_group is None:
250-
return None, "Missing exploration mode choice", [json.dumps(places_stack)]
215+
return None, "Missing exploration mode choice", spectrum_list, previous_mode
251216

252217
dataset = get_dataset(ctx, dataset_id)
253218
has_point = any(
@@ -257,17 +222,18 @@ def update_plot(
257222
)
258223

259224
if dataset is None:
260-
return None, "Missing dataset selection", [json.dumps(places_stack)]
225+
return None, "Missing dataset selection", spectrum_list, previous_mode
261226
elif not place_group or not has_point:
262-
return None, "Missing point selection", [json.dumps(places_stack)]
227+
return None, "Missing point selection", spectrum_list, previous_mode
263228

264229
label = find_selected_point_label(place_group, place_geo)
265230

266231
if label is None:
267232
return (
268233
None,
269234
"There is no label for the selected point",
270-
[json.dumps(places_stack)],
235+
spectrum_list,
236+
previous_mode,
271237
)
272238

273239
if place_geo.get("type") == "Point":
@@ -285,14 +251,16 @@ def update_plot(
285251
]
286252
)
287253
else:
288-
return None, "Selected geometry must be a point", [json.dumps(places_stack)]
254+
return None, "Selected geometry must be a point", spectrum_list, previous_mode
289255

290256
place_group_geodf["time"] = pd.to_datetime(time_label).tz_localize(None)
291257
places_select = [label]
292258
new_spectrum_data = get_spectra(dataset, place_group_geodf, places_select)
293259

294260
if new_spectrum_data is None or new_spectrum_data.empty:
295-
return None, "No reflectances found in Variables", [json.dumps(places_stack)]
261+
return None, "No reflectances found in Variables", spectrum_list, previous_mode
262+
263+
new_spectrum_data["legend"] = new_spectrum_data["places"] + ": " + time_label
296264

297265
existing_data = extract_data_from_chart(current_chart)
298266

@@ -304,63 +272,41 @@ def update_plot(
304272
}
305273
existing_data = filter_data_by_valid_labels(existing_data, valid_labels)
306274

307-
if exploration_radio_group == "active":
308-
if places_stack:
309-
existing_data, places_stack = remove_last_added_place(
310-
existing_data, places_stack
275+
if exploration_radio_group == "update":
276+
if previous_mode == "add":
277+
existing_data = pd.DataFrame()
278+
else:
279+
existing_data, spectrum_list = remove_last_added_place(
280+
existing_data, spectrum_list
311281
)
312282

313283
updated_data = add_place_data_to_existing(existing_data, new_spectrum_data)
314-
places_stack.append([label])
284+
if spectrum_list is None:
285+
spectrum_list = []
286+
spectrum_list.append(label)
315287
else:
316288
updated_data = add_place_data_to_existing(existing_data, new_spectrum_data)
317-
places_stack.append([label])
318-
319-
new_chart = create_chart_from_data(updated_data)
320289

321-
return new_chart, "", [json.dumps(places_stack)]
290+
# Vega Altair doesn’t support xOffset with x:Q, so we manually shift each bar
291+
# slightly
292+
unique_groups = sorted(updated_data["legend"].unique())
293+
n_groups = len(unique_groups)
294+
group_offset_map = {
295+
group: i - (n_groups - 1) / 2 for i, group in enumerate(unique_groups)
296+
}
322297

298+
bar_spacing = 3
299+
updated_data["x_offset"] = updated_data.apply(
300+
lambda row: row["wavelength"] + group_offset_map[row["legend"]] * bar_spacing,
301+
axis=1,
302+
)
323303

324-
@panel.callback(
325-
Input("delete_button", "clicked"),
326-
State("plot", "chart"),
327-
State("places_stack_storage", "children"),
328-
Output("plot", "chart"),
329-
Output("places_stack_storage", "children"),
330-
)
331-
def delete_places(
332-
ctx: Context,
333-
_clicked: bool | None = None,
334-
current_chart: alt.Chart | None = None,
335-
places_stack_json: list | None = None,
336-
) -> tuple[alt.Chart, list]:
337-
places_stack = []
338-
if places_stack_json and len(places_stack_json) > 0:
339-
try:
340-
places_stack = json.loads(places_stack_json[0])
341-
except (json.JSONDecodeError, IndexError):
342-
places_stack = []
343-
344-
current_data = extract_data_from_chart(current_chart)
345-
updated_data, updated_stack = remove_last_added_place(current_data, places_stack)
346304
new_chart = create_chart_from_data(updated_data)
347-
return new_chart, [json.dumps(updated_stack)]
348-
305+
previous_mode = exploration_radio_group
306+
return new_chart, "", spectrum_list, previous_mode
349307

350-
@panel.callback(
351-
Input("@app", "selectedPlaceGeometry"),
352-
Input("exploration_radio_group", "value"),
353-
Output("delete_button", "disabled"),
354-
)
355-
def set_button_disablement(
356-
_ctx: Context,
357-
place_geometry: str | None = None,
358-
exploration_radio_group: str | None = None,
359-
) -> bool:
360-
return not place_geometry and exploration_radio_group != "save"
361308

362-
363-
def find_selected_point_label(features_data, target_point):
309+
def find_selected_point_label(features_data, target_point) -> str | None:
364310
for feature_collection in features_data:
365311
for feature in feature_collection.get("features", []):
366312
geometry = feature.get("geometry", {})
@@ -399,21 +345,21 @@ def create_chart_from_data(data: pd.DataFrame) -> alt.Chart:
399345
x="wavelength:N",
400346
y="reflectance:Q",
401347
xOffset="places:N",
402-
color="places:N",
403-
tooltip=["variable", "wavelength", "reflectance"],
348+
color="legend:N",
349+
tooltip=["places", "variable", "wavelength", "reflectance"],
404350
)
405351
.properties(width="container", height="container")
406352
)
407353

408354
return (
409355
alt.Chart(data)
410-
.mark_bar()
356+
.mark_bar(size=2)
411357
.encode(
412-
x="wavelength:N",
413-
y="reflectance:Q",
414-
xOffset="places:N",
415-
color="places:N",
416-
tooltip=["variable", "wavelength", "reflectance"],
358+
x=alt.X("x_offset:Q", title="Wavelength"),
359+
y=alt.Y("reflectance:Q", title="Reflectance"),
360+
xOffset="legend:N",
361+
color="legend:N",
362+
tooltip=["places", "variable", "wavelength", "reflectance"],
417363
)
418364
).properties(width="container", height="container")
419365

@@ -424,6 +370,9 @@ def add_place_data_to_existing(
424370
if new_data.empty:
425371
return existing_data
426372

373+
if existing_data.empty:
374+
return new_data
375+
427376
# This is to check if the new_data already exists in the existing_data to avoid
428377
# duplication
429378
if not existing_data.empty:
@@ -432,18 +381,27 @@ def add_place_data_to_existing(
432381
return existing_data
433382

434383
combined_data = pd.concat([existing_data, new_data], ignore_index=True)
435-
return combined_data
384+
385+
# Throttling to last 10 spectrum views
386+
final_df = combined_data[
387+
combined_data["places"].isin(
388+
combined_data.drop_duplicates("places", keep="last").tail(
389+
THROTTLE_TOTAL_SPECTRUM_PLOTS
390+
)["places"]
391+
)
392+
]
393+
return final_df
436394

437395

438396
def remove_last_added_place(
439-
data: pd.DataFrame, places_stack: list
397+
data: pd.DataFrame, spectrum_list: list
440398
) -> tuple[pd.DataFrame, list]:
441-
if not places_stack or data.empty:
442-
return data, places_stack
399+
if not spectrum_list or data.empty:
400+
return data, spectrum_list
443401

444-
last_places = places_stack.pop()
445-
filtered_data = data[~data["places"].isin(last_places)]
446-
return filtered_data, places_stack
402+
last_places = spectrum_list.pop()
403+
filtered_data = data[~data["places"].isin([last_places])]
404+
return filtered_data, spectrum_list
447405

448406

449407
def filter_data_by_valid_labels(data: pd.DataFrame, valid_labels: set) -> pd.DataFrame:

0 commit comments

Comments
 (0)