11import math
2- import json
32from typing import Any
43
54import altair as alt
1413from chartlets import Component , Input , State , Output
1514from chartlets .components import (
1615 Box ,
17- Button ,
1816 Typography ,
1917 VegaChart ,
2018 Radio ,
2927
3028panel = 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)
230202def 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
438396def 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
449407def filter_data_by_valid_labels (data : pd .DataFrame , valid_labels : set ) -> pd .DataFrame :
0 commit comments