From 5ea407bccdfabe1b91c3ae4339eea64977580b4f Mon Sep 17 00:00:00 2001 From: Jochem Smit Date: Wed, 30 Oct 2024 12:54:41 +0100 Subject: [PATCH] file selectors --- dont_fret/web/bursts/components.py | 136 ++++++++++++++--------------- dont_fret/web/bursts/page.py | 14 +-- dont_fret/web/components.py | 82 +++++++++++++++++ dont_fret/web/new_models.py | 4 +- dont_fret/web/state.py | 7 +- dont_fret/web/trace/page.py | 13 +-- 6 files changed, 163 insertions(+), 93 deletions(-) diff --git a/dont_fret/web/bursts/components.py b/dont_fret/web/bursts/components.py index 616920a..dd2d9fc 100644 --- a/dont_fret/web/bursts/components.py +++ b/dont_fret/web/bursts/components.py @@ -21,7 +21,7 @@ import dont_fret.web.state as state from dont_fret.web.bursts.methods import create_histogram -from dont_fret.web.components import RangeInputField +from dont_fret.web.components import FigureFromTask, RangeInputField, RegexSelectDialog from dont_fret.web.methods import chain_filters from dont_fret.web.models import BinnedImage, BurstFilterItem, BurstNode, BurstPlotSettings from dont_fret.web.new_models import FRETNode, FRETStore, ListStore @@ -267,13 +267,15 @@ def on_field(value): @solara.component -def FileFilterDialog( - burst_item: solara.Reactive[BurstNode], +def FileFilterDialogDepr( + value: list[str], + on_value: Callable[[list[str]], None], + values: list[str], on_close: Callable[[], None], ): """update; selected files is stored elsewhere now""" all_files = sorted(burst_item.value.df["filename"].unique()) - local_selected_files = solara.use_reactive(cast(list[str], burst_item.value.selected_files)) + local_selected_files = solara.use_reactive(value) error, set_error = solara.use_state("") regex, set_regex = solara.use_state("") @@ -590,14 +592,6 @@ def set_selected_files(self, values: list[str]): self.selection_store.value = new_value -# = VIEW -# REFACTOR -# def BurstFigure( -# seletion: is not a simple dataclass with reactives only -# node_tree = fret_store derived value which is updated when the node tree is updated (and thus reredner) -# ) - - def generate_figure( df: pl.DataFrame, plot_settings: BurstPlotSettings, @@ -675,89 +669,87 @@ def generate_figure( @solara.component def BurstFigure( - selection: BurstFigureSelection, + selection: ListStore[str], + file_selection: dict[uuid.UUID, ListStore[str]], ): - figure, set_figure = solara.use_state(cast(Optional[go.Figure], None)) - edit_settings = solara.use_reactive(False) + settings_dialog = solara.use_reactive(False) + file_filter_dialog = solara.use_reactive(False) plot_settings = solara.use_reactive( BurstPlotSettings() - ) # -> these reset to default, combine with burstfigureselection? + ) # -> these reset to default, move to global state? dark_effective = solara.lab.use_dark_effective() - selected_file_names = [ - node.name - for node in selection.burst_node.photon_nodes - if node.id.hex in selection.selected_files - ] - file_filter = pl.col("filename").is_in(selected_file_names) + labels = ["Measurement", "Bursts"] # TODO move elsewhere + selector_nodes = make_selector_nodes(state.fret_nodes.items, "bursts") + # making the levels populates the selection + levels = list(NestedSelectors(nodes=selector_nodes, selection=selection, labels=labels)) + burst_node = get_bursts(state.fret_nodes.items, selection.items) + filenames = sorted(burst_node.df["filename"].unique()) + if burst_node.id in file_selection: + file_store = file_selection[burst_node.id] + else: + file_store = ListStore(filenames) + file_selection[burst_node.id] = file_store + + # file_store = file_selection[ + # burst_node.id + # ] # we make a new one here if it doestn exist yet, but should be OK + + # # it should never be empty, only after making a new one, thus we set it to all selected if its emtp + + # solara.Text(str(burst_node.id)) + + file_filter = pl.col("filename").is_in(file_store.items) f_expr = chain_filters(state.filters.items) & file_filter # this is triggered twice ? -> known plotly bug, use .key(...) def redraw(): - filtered_df = selection.burst_node.df.filter(f_expr) + print("redraw") + filtered_df = burst_node.df.filter(f_expr) img = BinnedImage.from_settings(filtered_df, plot_settings.value) figure = generate_figure( filtered_df, plot_settings.value, binned_image=img, dark=dark_effective ) - set_figure(figure) + return figure - # todo upgrade to use_task - fig_result = solara.use_thread( + figure_task = solara.lab.use_task( redraw, - dependencies=[ - selection.burst_id.value, - selection.selected_files, - plot_settings.value, - state.filters.items, - dark_effective, - ], - intrusive_cancel=False, # is much faster + dependencies=[burst_node.id, plot_settings.value, file_store.items, state.filters.items], ) with solara.Card(): with solara.Row(): - solara.Select( - label="Measurement", # TODO refactor to FRET node? - value=selection.fret_id.value.hex, - on_value=selection.set_fret_id, # type: ignore - values=selection.fret_values, # type: ignore - ) + for level in levels: + solara.Select(**level) - solara.Select( - label="Burst item", - value=selection.burst_id.value.hex, - on_value=selection.set_burst_id, - values=selection.burst_values, # type: ignore - ) - SelectCount( - label="Files", - value=selection.selected_files, - on_value=selection.set_selected_files, - items=selection.selected_files_values, # type: ignore - item_name="file", + solara.IconButton( + icon_name="mdi-file-star", on_click=lambda: file_filter_dialog.set(True) ) - # solara.IconButton(icon_name="mdi-file-star", on_click=lambda: set_edit_filter(True)) - solara.IconButton(icon_name="mdi-settings", on_click=lambda: edit_settings.set(True)) - - solara.ProgressLinear(fig_result.state == solara.ResultState.RUNNING) - if figure is not None: - with solara.Div( - style="opacity: 0.3" if fig_result.state == solara.ResultState.RUNNING else None - ): - solara.FigurePlotly(figure) + solara.IconButton(icon_name="mdi-settings", on_click=lambda: settings_dialog.set(True)) + + FigureFromTask(figure_task) + + with solara.v.Dialog( + v_model=settings_dialog.value, max_width=750, on_v_model=settings_dialog.set + ): + PlotSettingsEditDialog( + plot_settings, + burst_node.df.filter(f_expr), # = filtered dataframe by global filter + on_close=lambda: settings_dialog.set(False), + duration=burst_node.duration, + ) - # dedent this and figure will flicker/be removed when opening the dialog - if edit_settings.value: - with rv.Dialog( - v_model=edit_settings.value, max_width=750, on_v_model=edit_settings.set - ): - PlotSettingsEditDialog( - plot_settings, - selection.burst_node.df.filter(f_expr), # = filtered dataframe by global filter - on_close=lambda: edit_settings.set(False), - duration=selection.burst_node.duration, - ) + with solara.v.Dialog( + v_model=file_filter_dialog.value, max_width=750, on_v_model=file_filter_dialog.set + ): + RegexSelectDialog( + title="File Filter", + value=file_store.items, + on_value=file_store.set, + values=filenames, + on_close=lambda: file_filter_dialog.set(False), + ) @solara.component diff --git a/dont_fret/web/bursts/page.py b/dont_fret/web/bursts/page.py index 8af58ec..8e679da 100644 --- a/dont_fret/web/bursts/page.py +++ b/dont_fret/web/bursts/page.py @@ -17,17 +17,19 @@ def BurstPage(): for filter_item in state.filters.items: FilterListItem(filter_item) - if open_filter_dialog: - with solara.v.Dialog( - v_model=open_filter_dialog.value, max_width=1200, on_v_model=open_filter_dialog.set - ): - with solara.Card(style={"width": "1000px"}): - FilterEditDialog() + # if open_filter_dialog: + with solara.v.Dialog( + v_model=open_filter_dialog.value, max_width=1200, on_v_model=open_filter_dialog.set + ): + with solara.Card(style={"width": "1190px"}): + FilterEditDialog() with solara.GridFixed(columns=2): BurstFigure( state.burst_figure_selection[0], + state.burst_figure_file_selection[0], ) BurstFigure( state.burst_figure_selection[1], + state.burst_figure_file_selection[1], ) diff --git a/dont_fret/web/components.py b/dont_fret/web/components.py index f1a9576..94de3e1 100644 --- a/dont_fret/web/components.py +++ b/dont_fret/web/components.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from typing import Callable, Optional, Type import solara @@ -114,3 +115,84 @@ def handle(*args): div = solara.Div(children=children) solara.v.use_event(div, "dblclick", handle) + + +@solara.component +def FigureFromTask(task: solara.lab.Task): + solara.ProgressLinear(task.pending) + if task.latest is None: + solara.Text("loading...") + else: + figure = task.value if task.finished else task.latest + with solara.Div(style="opacity: 0.3" if task.pending else None): + solara.FigurePlotly(figure) + + +@solara.component +def RegexSelectDialog( + title: str, + value: list[str], + on_value: Callable[[list[str]], None], + values: list[str], + on_close: Callable[[], None], + sort: bool = True, +): + """ + select string by checkboxes or regex + """ + local_selection = solara.use_reactive(value) + error = solara.use_reactive("") + regex = solara.use_reactive("") + + def on_input(value: str): + try: + pattern = re.compile(value) + regex.set(value) + error.set("") + except Exception: + error.set("Invalid regex") + return + new_selected = [f for f in values if pattern.search(f)] + local_selection.set(new_selected) + + def on_save(): + if not local_selection.value: + return + if sort: + on_value(sorted(local_selection.value)) + else: + on_value(local_selection.value) + on_close() + + with solara.Card(title): + with solara.Row(style="align-items: center;"): + solara.InputText( + label="regex", + value=regex.value, + on_value=on_input, + continuous_update=True, + error=error.value, + ) + solara.Button(label="Select All", on_click=lambda: local_selection.set(values)) + solara.Button(label="Select None", on_click=lambda: local_selection.set([])) + with solara.v.List(nav=True): + with solara.v.ListItemGroup( + v_model=local_selection.value, + on_v_model=local_selection.set, + multiple=True, + ): + for v in values: + with solara.v.ListItem(value=v): + with solara.v.ListItemAction(): + solara.Checkbox(value=v in local_selection.value) + solara.v.ListItemTitle(children=[v]) + + with solara.CardActions(): + solara.v.Spacer() + solara.Button( + "Save", + icon_name="mdi-content-save", + on_click=on_save, + disabled=not local_selection.value, + ) + solara.Button("Close", icon_name="mdi-window-close", on_click=on_close) diff --git a/dont_fret/web/new_models.py b/dont_fret/web/new_models.py index b87a941..f35d3a1 100644 --- a/dont_fret/web/new_models.py +++ b/dont_fret/web/new_models.py @@ -51,8 +51,8 @@ class _NoDefault: class ListStore(Generic[T]): """baseclass for reactive list""" - def __init__(self, items: list[T]): - self._items = solara.reactive(items) + def __init__(self, items: Optional[list[T]] = None): + self._items = solara.reactive(items if items is not None else []) def __len__(self): return len(self.items) diff --git a/dont_fret/web/state.py b/dont_fret/web/state.py index a301657..85611cf 100644 --- a/dont_fret/web/state.py +++ b/dont_fret/web/state.py @@ -1,6 +1,7 @@ import copy import dataclasses import uuid +from collections import defaultdict from pathlib import Path from typing import Callable, Generic, Type, TypeVar @@ -37,10 +38,12 @@ fret_nodes = FRETStore(TEST_NODES) burst_figure_selection = [ - BurstFigureSelection(fret_nodes), - BurstFigureSelection(fret_nodes), + ListStore[str]([]), + ListStore[str]([]), ] +burst_figure_file_selection = [{}, {}] + trace_selection = PhotonNodeSelection(fret_nodes) # cfg set to dask manager diff --git a/dont_fret/web/trace/page.py b/dont_fret/web/trace/page.py index 5b5dae9..0606f2b 100644 --- a/dont_fret/web/trace/page.py +++ b/dont_fret/web/trace/page.py @@ -13,6 +13,7 @@ import dont_fret.web.state as state from dont_fret import BinnedPhotonData, PhotonData from dont_fret.formatting import TRACE_COLORS, TRACE_SIGNS +from dont_fret.web.components import FigureFromTask from dont_fret.web.methods import generate_traces from dont_fret.web.models import ( BurstNode, @@ -85,6 +86,7 @@ class PhotonNodeSelection: ) def __post_init__(self): + print("deprecate") # i want to remove this, we can check if the current id is in selection when accessing the property self.fret_store._items.subscribe(self.on_fret_store) self.reset() @@ -165,17 +167,6 @@ def TracePage(): TCSPCFigure(photon_node) -@solara.component -def FigureFromTask(task: solara.lab.Task): - solara.ProgressLinear(task.pending) - if task.latest is None: - solara.Text("loading...") - else: - figure = task.value if task.finished else task.latest - with solara.Div(style="opacity: 0.3" if task.pending else None): - solara.FigurePlotly(figure) - - @solara.component def TraceFigure(photon_node: PhotonNode, settings: TraceSettings): dark_effective = solara.lab.use_dark_effective()