Skip to content

Commit cb15f99

Browse files
committed
remove datamanager and use chart as state instead
1 parent 095c8b3 commit cb15f99

File tree

1 file changed

+150
-103
lines changed

1 file changed

+150
-103
lines changed

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

Lines changed: 150 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import math
2+
import json
23
from typing import Any
34

45
import altair as alt
@@ -29,41 +30,6 @@
2930
panel = 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
)
261230
def 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
)
360331
def 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

Comments
 (0)