11import math
2+ import json
23from typing import Any
34
45import altair as alt
2930panel = Panel (__name__ , title = "Spectrum View (Demo)" , icon = "light" , position = 4 )
3031
3132
32- class DataManager :
33- def __init__ (self ):
34- self .all_data = pd .DataFrame (
35- columns = ["places" , "variable" , "reflectance" , "wavelength" ]
36- )
37- self .added_places_stack = []
38-
39- def add_place_data (self , new_data : pd .DataFrame ):
40- if self ._is_duplicate (new_data ):
41- return
42-
43- place_names = new_data ["places" ].unique ()
44- self .all_data = pd .concat ([self .all_data , new_data ], ignore_index = True )
45- self .added_places_stack .append (place_names )
46-
47- def remove_last_added_place (self ):
48- if self .added_places_stack :
49- last_places = self .added_places_stack .pop ()
50- self .all_data = self .all_data [~ self .all_data ["places" ].isin (last_places )]
51-
52- def _is_duplicate (self , new_data : pd .DataFrame ) -> bool :
53- merged = new_data .merge (self .all_data , how = "left" , indicator = True )
54- return (merged ["_merge" ] == "both" ).all ()
55-
56- def delete_all_data (self ):
57- self .all_data = pd .DataFrame (
58- columns = ["places" , "variable" , "reflectance" , "wavelength" ]
59- )
60- self .added_places_stack = []
61-
62-
63- manager = DataManager ()
64- previous_mode = ""
65-
66-
6733@panel .layout (
6834 State ("@app" , "selectedDatasetId" ),
6935 State ("@app" , "selectedTimeLabel" ),
@@ -75,7 +41,6 @@ def render_panel(
7541 time_label : str ,
7642 theme_mode : str ,
7743) -> Component :
78-
7944 if theme_mode == "light" :
8045 theme_mode = "default"
8146
@@ -152,13 +117,19 @@ def render_panel(
152117 id = "error_message" , style = {"color" : "red" }, children = ["" ]
153118 )
154119
120+ places_stack_storage = Typography (
121+ id = "places_stack_storage" ,
122+ children = ["[]" ],
123+ style = {"display" : "none" },
124+ )
125+
155126 return Box (
156127 children = [
157- "Select a map point from the dropdown and press 'Update' "
158- "to create a spectrum plot for that point and the selected time." ,
128+ "Choose an exploration mode and create/select points to view the Spectrum data." ,
159129 control_bar ,
160130 error_message ,
161131 plot ,
132+ places_stack_storage ,
162133 ],
163134 style = {
164135 "display" : "flex" ,
@@ -176,7 +147,6 @@ def get_spectra(
176147 place_group : gpd .GeoDataFrame ,
177148 places : list ,
178149) -> pd .DataFrame :
179-
180150 grid_mapping = GridMapping .from_dataset (dataset )
181151
182152 project = pyproj .Transformer .from_crs (
@@ -198,7 +168,6 @@ def get_spectra(
198168 result = pd .DataFrame ()
199169
200170 for place in places :
201-
202171 i = (dataset_place .name_ref == place ).argmax ().item ()
203172 selected_values = (
204173 dataset_place .drop_vars ("geometry_ref" )
@@ -241,9 +210,8 @@ def update_text(
241210 dataset_title : str | None = None ,
242211 time_label : str | None = None ,
243212) -> list | None :
244-
245213 if time_label :
246- return [f"{ dataset_title } " f" / { time_label [0 :- 1 ]} " ]
214+ return [f"{ dataset_title } / { time_label [0 :- 1 ]} " ]
247215 return [f"{ dataset_title } " ]
248216
249217
@@ -253,40 +221,55 @@ def update_text(
253221 Input ("@app" , "selectedPlaceGeometry" ),
254222 State ("@app" , "selectedPlaceGroup" ),
255223 State ("exploration_radio_group" , "value" ),
256- # Input("add_button ", "clicked "),
257- Input ( "plot " , "chart " ),
224+ State ( "plot " , "chart " ),
225+ State ( "places_stack_storage " , "children " ),
258226 Output ("plot" , "chart" ),
259227 Output ("error_message" , "children" ),
228+ Output ("places_stack_storage" , "children" ),
260229)
261230def update_plot (
262231 ctx : Context ,
263232 dataset_id : str | None = None ,
264233 time_label : str | None = None ,
265234 place_geo : dict [str , Any ] | None = None ,
266235 place_group : list [dict [str , Any ]] | None = None ,
267- exploration_radio_group : bool | None = None ,
268- # _clicked: bool | None = None,
269- previous_chart : bool | None = None ,
270- ) -> tuple [alt .Chart | None , str ]:
271- global manager , previous_mode
272- # print("clicked::", _clicked)
236+ exploration_radio_group : str | None = None ,
237+ 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+
273249 if exploration_radio_group is None :
274- return None , "Missing exploration mode choice"
250+ return None , "Missing exploration mode choice" , [json .dumps (places_stack )]
251+
275252 dataset = get_dataset (ctx , dataset_id )
276253 has_point = any (
277254 feature .get ("geometry" , {}).get ("type" ) == "Point"
278255 for collection in place_group
279256 for feature in collection .get ("features" , [])
280257 )
258+
281259 if dataset is None :
282- return None , "Missing dataset selection"
260+ return None , "Missing dataset selection" , [ json . dumps ( places_stack )]
283261 elif not place_group or not has_point :
284- return None , "Missing point selection"
262+ return None , "Missing point selection" , [ json . dumps ( places_stack )]
285263
286264 label = find_selected_point_label (place_group , place_geo )
287265
288266 if label is None :
289- return None , "There is no label for the selected point"
267+ return (
268+ None ,
269+ "There is no label for the selected point" ,
270+ [json .dumps (places_stack )],
271+ )
272+
290273 if place_geo .get ("type" ) == "Point" :
291274 place_group_geodf = gpd .GeoDataFrame (
292275 [
@@ -302,78 +285,66 @@ def update_plot(
302285 ]
303286 )
304287 else :
305- return None , "Selected geometry must be a point"
288+ return None , "Selected geometry must be a point" , [ json . dumps ( places_stack )]
306289
307290 place_group_geodf ["time" ] = pd .to_datetime (time_label ).tz_localize (None )
308291 places_select = [label ]
309- source = get_spectra (dataset , place_group_geodf , places_select )
292+ new_spectrum_data = get_spectra (dataset , place_group_geodf , places_select )
310293
311- if source is None :
312- return None , "No reflectances found in Variables"
313- if previous_chart is None :
314- manager .delete_all_data ()
294+ if new_spectrum_data is None or new_spectrum_data .empty :
295+ return None , "No reflectances found in Variables" , [json .dumps (places_stack )]
315296
316- # Delete points that are removed by the user from the viewer:
297+ existing_data = extract_data_from_chart (current_chart )
298+
299+ # Filter points in case the user deletes them.
317300 valid_labels = {
318301 feature ["properties" ]["label" ]
319302 for item in place_group
320303 for feature in item .get ("features" , [])
321304 }
322- manager . all_data = manager . all_data [ manager . all_data [ "places" ]. isin ( valid_labels )]
305+ existing_data = filter_data_by_valid_labels ( existing_data , valid_labels )
323306
324307 if exploration_radio_group == "active" :
325- if previous_mode != "save" :
326- manager .remove_last_added_place ()
327- manager .add_place_data (source )
328- chart = (
329- alt .Chart (manager .all_data )
330- .mark_bar ()
331- .encode (
332- x = "wavelength:N" ,
333- y = "reflectance:Q" ,
334- xOffset = "places:N" ,
335- color = "places:N" ,
336- tooltip = ["variable" , "wavelength" , "reflectance" ],
308+ if places_stack :
309+ existing_data , places_stack = remove_last_added_place (
310+ existing_data , places_stack
337311 )
338- ).properties (width = "container" , height = "container" )
312+
313+ updated_data = add_place_data_to_existing (existing_data , new_spectrum_data )
314+ places_stack .append ([label ])
339315 else :
340- manager .add_place_data (source )
341- chart = (
342- alt .Chart (manager .all_data )
343- .mark_bar ()
344- .encode (
345- x = "wavelength:N" ,
346- y = "reflectance:Q" ,
347- xOffset = "places:N" ,
348- color = "places:N" ,
349- tooltip = ["variable" , "wavelength" , "reflectance" ],
350- )
351- ).properties (width = "container" , height = "container" )
352- previous_mode = exploration_radio_group
353- return chart , ""
316+ 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 )
320+
321+ return new_chart , "" , [json .dumps (places_stack )]
354322
355323
356324@panel .callback (
357325 Input ("delete_button" , "clicked" ),
326+ State ("plot" , "chart" ),
327+ State ("places_stack_storage" , "children" ),
358328 Output ("plot" , "chart" ),
329+ Output ("places_stack_storage" , "children" ),
359330)
360331def delete_places (
361332 ctx : Context ,
362333 _clicked : bool | None = None ,
363- ) -> alt .Chart :
364- global manager
365- manager . remove_last_added_place ()
366- return (
367- alt . Chart ( manager . all_data )
368- . mark_bar ()
369- . encode (
370- x = "wavelength:N" ,
371- y = "reflectance:Q" ,
372- xOffset = "places:N" ,
373- color = "places:N" ,
374- tooltip = [ "variable" , "wavelength" , "reflectance" ],
375- )
376- ). properties ( width = "container" , height = "container" )
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 )
346+ new_chart = create_chart_from_data ( updated_data )
347+ return new_chart , [ json . dumps ( updated_stack )]
377348
378349
379350@panel .callback (
@@ -403,3 +374,79 @@ def find_selected_point_label(features_data, target_point):
403374 return feature .get ("properties" , {}).get ("label" , None )
404375
405376 return None
377+
378+
379+ def extract_data_from_chart (chart : alt .Chart ) -> pd .DataFrame :
380+ if chart is None :
381+ return pd .DataFrame (columns = ["places" , "variable" , "reflectance" , "wavelength" ])
382+
383+ if chart .get ("datasets" , {}) != {}:
384+ return pd .DataFrame (list (chart .get ("datasets" ).values ())[0 ])
385+
386+ return pd .DataFrame (columns = ["places" , "variable" , "reflectance" , "wavelength" ])
387+
388+
389+ def create_chart_from_data (data : pd .DataFrame ) -> alt .Chart :
390+ if data .empty :
391+ return (
392+ alt .Chart (
393+ pd .DataFrame (
394+ columns = ["places" , "variable" , "reflectance" , "wavelength" ]
395+ )
396+ )
397+ .mark_bar ()
398+ .encode (
399+ x = "wavelength:N" ,
400+ y = "reflectance:Q" ,
401+ xOffset = "places:N" ,
402+ color = "places:N" ,
403+ tooltip = ["variable" , "wavelength" , "reflectance" ],
404+ )
405+ .properties (width = "container" , height = "container" )
406+ )
407+
408+ return (
409+ alt .Chart (data )
410+ .mark_bar ()
411+ .encode (
412+ x = "wavelength:N" ,
413+ y = "reflectance:Q" ,
414+ xOffset = "places:N" ,
415+ color = "places:N" ,
416+ tooltip = ["variable" , "wavelength" , "reflectance" ],
417+ )
418+ ).properties (width = "container" , height = "container" )
419+
420+
421+ def add_place_data_to_existing (
422+ existing_data : pd .DataFrame , new_data : pd .DataFrame
423+ ) -> pd .DataFrame :
424+ if new_data .empty :
425+ return existing_data
426+
427+ # This is to check if the new_data already exists in the existing_data to avoid
428+ # duplication
429+ if not existing_data .empty :
430+ merged = new_data .merge (existing_data , how = "left" , indicator = True )
431+ if (merged ["_merge" ] == "both" ).all ():
432+ return existing_data
433+
434+ combined_data = pd .concat ([existing_data , new_data ], ignore_index = True )
435+ return combined_data
436+
437+
438+ def remove_last_added_place (
439+ data : pd .DataFrame , places_stack : list
440+ ) -> tuple [pd .DataFrame , list ]:
441+ if not places_stack or data .empty :
442+ return data , places_stack
443+
444+ last_places = places_stack .pop ()
445+ filtered_data = data [~ data ["places" ].isin (last_places )]
446+ return filtered_data , places_stack
447+
448+
449+ def filter_data_by_valid_labels (data : pd .DataFrame , valid_labels : set ) -> pd .DataFrame :
450+ if data .empty :
451+ return data
452+ return data [data ["places" ].isin (valid_labels )]
0 commit comments