CoolFace
Apppublic

mlukac/xrf-explorer-dev

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
combined_controller.py759 linesDownload Raw Back to controllers
1from dataclasses import dataclass2from typing import Any, Callable3 4import param5from panel.widgets import MenuButton6 7from xrf_explorer.common.defaults import DEFAULT_FIGURES_CONFIG, DEFAULT_CURVE_GROUPS8from xrf_explorer.core.events import CombCtrlUpdated, FileLoadHandled, NormalizationUpdated, UpdateVizColumns, FigureNormalization9from xrf_explorer.core.types import ApplicationHandlerProtocol, Curve, DatasetType10from xrf_explorer.helpers.print import time_me11 12TCurveGroup = dict[DatasetType, list[str]]13TExportCurveGroup = dict[DatasetType, list[Curve]]14TLoadedCurves = dict[DatasetType, dict[str, Curve]]15 16 17class CombCurveController(param.Parameterized):18 19    set_group = param.Event(label="save as new group")20    add_fig = param.Event(label="add new figure")21 22    fig_name = param.Selector()23    editable_fig_name = param.String()24    25    normalize_xrf = param.Boolean(default=False, label="Normalize XRF")26 27    xrf_selector = param.ListSelector()28    mass_spec_selector = param.ListSelector()29    gamma_ray_selector = param.ListSelector()30    drilling_selector = param.ListSelector()31 32    def __init__(self, app: ApplicationHandlerProtocol, **kwargs) -> None:33        super().__init__(**kwargs)34        self._app = app35        self._figure_manager = _FigureManager(self)36        self._group_manager = _CurveGroupManager(self)37        self._slctr_manager = _SelectorManager(38            self,39            [40                _SelectorMeta(41                    dataset_type=DatasetType.XRF,42                    parameter=self.param.xrf_selector,43                    value_getter=lambda: self.xrf_selector,44                ),45                _SelectorMeta(46                    dataset_type=DatasetType.MASS_SPEC,47                    parameter=self.param.mass_spec_selector,48                    value_getter=lambda: self.mass_spec_selector,49                ),50                _SelectorMeta(51                    dataset_type=DatasetType.GAMMA_RAY,52                    parameter=self.param.gamma_ray_selector,53                    value_getter=lambda: self.gamma_ray_selector,54                ),55                _SelectorMeta(56                    dataset_type=DatasetType.DRILLING,57                    parameter=self.param.drilling_selector,58                    value_getter=lambda: self.drilling_selector,59                ),60            ],61        )62 63        self._group_dropdown_button = MenuButton(64            name="Set Curves To",65            items=self._group_manager.group_names,66            icon="transfer",67            icon_size="18px",68        )69        self._group_dropdown_button.on_click(self._set_group_from_dropdown)70 71        # dropdown menu to select figure to remove72        self._remove_fig_dropdown_button = MenuButton(73            name="Remove",74            items=self._figure_manager.fig_names,75            icon="square-rounded-minus",76            icon_size="18px",77        )78        self._remove_fig_dropdown_button.on_click(self._remove_figure)79 80        self._discard_figure_update: bool = False81 82    @property83    def _loaded_curves(self) -> TLoadedCurves:84        return self._app.curves.curves85 86    @property87    def group_dropdown_button(self) -> MenuButton:88        return self._group_dropdown_button89 90    @property91    def remove_fig_dropdown_button(self) -> MenuButton:92        return self._remove_fig_dropdown_button93 94    @property95    def figures(self) -> dict[str, TExportCurveGroup]:96        return self._figure_manager.exportable_figures97    98    def get_current_figure_normalization_state(self) -> bool:99        return self._figure_manager.get_normalization_state(self.fig_name)100 101    def handle_file_load(self, event: FileLoadHandled) -> None:102        self._slctr_manager.sync_selector(event.dataset_type)103 104        # Create/update figures from DEFAULT_FIGURES_CONFIG105        self._update_figures_from_config(event.dataset_type)106 107    def _update_figures_from_config(self, dataset_type: DatasetType) -> None:108        """Create or update figures based on DEFAULT_FIGURES_CONFIG for the given dataset type."""109        from xrf_explorer.common.defaults import DEFAULT_FIGURES_CONFIG110        111        available_curves = list(self._loaded_curves[dataset_type].keys())112        first_created_figure = None113        114        # Batch all figure updates in single context to minimize view rebuilds115        with _VizCurveUpdateContext(self):116            for fig_name, fig_config in DEFAULT_FIGURES_CONFIG.items():117                # Skip figures that have no curves for this dataset type118                config_curves = fig_config.get(dataset_type, [])119                if not config_curves:120                    continue121                    122                # Filter to curves that actually exist in the loaded file123                valid_curves = [c for c in config_curves if c in available_curves]124                if not valid_curves:125                    continue126                    127                # Check if figure already exists128                if fig_name in self._figure_manager.figures:129                    # Figure exists - merge new curves with existing selections130                    existing_curves = self._figure_manager.get_figure(fig_name)131                    existing_dataset_curves = existing_curves.get(dataset_type, [])132                    133                    # Merge curves, avoiding duplicates134                    merged_curves = list(existing_dataset_curves)135                    for curve in valid_curves:136                        if curve not in merged_curves:137                            merged_curves.append(curve)138                            139                    # Update the existing figure with merged curves (no individual viz trigger)140                    updated_curves = existing_curves.copy()141                    updated_curves[dataset_type] = merged_curves142                    self._figure_manager.set_figure(fig_name, updated_curves, auto_trigger_viz=False)143                else:144                    # Figure doesn't exist - create new figure with these curves (no individual viz trigger)145                    new_figure_curves = {dataset_type: valid_curves}146                    self._figure_manager.add_new_figure(fig_name, new_figure_curves, auto_trigger_viz=False)147                    148                    if first_created_figure is None:149                        first_created_figure = fig_name150        151        # Set the first created figure as active (if any were created)152        if first_created_figure is not None:153            self._figure_manager.fig_name = first_created_figure154        155        # Set normalization states for specific figures that should be normalized by default156        figures_to_normalize = ["Major Elements", "Redox Elements"]157        for fig_name in figures_to_normalize:158            if fig_name in self._figure_manager.figures:159                self._figure_manager.set_normalization_state(fig_name, True)160        161        # Sync UI checkbox for currently selected figure162        if self.fig_name and self.fig_name in figures_to_normalize:163            self._discard_figure_update = True164            self.normalize_xrf = self._figure_manager.get_normalization_state(self.fig_name)165            self._discard_figure_update = False166        167        # Compute normalized columns for all figures that have normalization enabled168        self.recompute_active_normalizations(None)169 170    @time_me()171    def _set_group_from_dropdown(self, p_event: Any) -> None:172        self._group_manager.update_group_name(str(p_event.new))173        self._set_group_from_dropdown_impl()174 175    def _set_group_from_dropdown_impl(self) -> None:176        self._discard_figure_update = True177        curve_group = self._group_manager.curve_group178        self._slctr_manager.set_curve_group(curve_group, manual_trigger=True)179        self._discard_figure_update = False180 181        self._update_figures_impl()182        self._app.trigger(CombCtrlUpdated())183 184    @time_me()185    def _remove_figure(self, p_event: Any) -> None:186        forsaken_fig_name = p_event.new187        self._figure_manager.remove_fig(forsaken_fig_name)188        self._remove_fig_dropdown_button.items = self._figure_manager.fig_names189        self._app.trigger(CombCtrlUpdated())190 191    @time_me()192    @param.depends("set_group", watch=True)193    def _set_to_group(self):194        self._group_manager.add_new_group(self._slctr_manager.get_curve_group())195        self._update_figures_impl()196        self._group_dropdown_button.items = self._group_manager.group_names197        self._app.trigger(CombCtrlUpdated())198 199    @time_me()200    @param.depends("add_fig", watch=True)201    def _add_new_figure(self):202        self._figure_manager.add_new_figure()203        self._update_fig_dropdown()204        self._app.trigger(CombCtrlUpdated())205 206    @time_me()207    @param.depends("fig_name", watch=True)208    def _change_figure_selectors(self):209        curve_group = self._figure_manager.get_figure(self.fig_name)210        211        # Suppress figure updates during UI sync212        self._discard_figure_update = True213        214        # Collect all parameter changes to batch them215        selector_params = self._slctr_manager.get_curve_group_params(curve_group)216        all_params = {217            **selector_params,218            'editable_fig_name': self.fig_name,219            'normalize_xrf': self._figure_manager.get_normalization_state(self.fig_name)220        }221        222        # Apply all parameter changes at once223        self.param.update(**all_params)224        225        self._discard_figure_update = False226 227    @time_me()228    @param.depends("editable_fig_name", watch=True)229    def _update_figure_name(self):230        if self._discard_figure_update:231            return232        if self._figure_manager.is_editable_fig_name_new():233            self._figure_manager.save_editable_name()234        self._update_fig_dropdown()235        self._app.trigger(CombCtrlUpdated())236 237    @time_me()238    @param.depends("normalize_xrf", watch=True)239    def _update_normalization(self) -> None:240        if self._discard_figure_update:241            return242        # Update the current figure's normalization state243        self._figure_manager.set_normalization_state(self.fig_name, self.normalize_xrf)244        245        # Gather normalization information for the event246        current_fig_name = self.fig_name247        fig_id = self._figure_manager.get_figure_id(current_fig_name)248        249        # Get XRF elements for the current figure250        figures = self.figures251        if figures and current_fig_name in figures:252            fig_curves = figures[current_fig_name]253            xrf_curves = fig_curves.get(DatasetType.XRF, [])254            element_names = [curve.name for curve in xrf_curves]255        else:256            element_names = []257        258        self._app.trigger(NormalizationUpdated(259            normalizations=[FigureNormalization(260                fig_id=fig_id,261                fig_name=current_fig_name,262                elements=element_names,263                normalize_enabled=self.normalize_xrf264            )]265        ))266 267    @time_me()268    @param.depends("xrf_selector", watch=True)269    def _handle_xrf_selector_change(self) -> None:270        if self._discard_figure_update:271            return272        273        # If normalization is currently enabled for this figure, disable it274        # since the XRF elements are changing275        if self._figure_manager.get_normalization_state(self.fig_name):276            print(f"[CombController] XRF elements changed - disabling normalization for figure '{self.fig_name}'")277            self._figure_manager.set_normalization_state(self.fig_name, False)278            279            # Update UI to reflect disabled normalization280            # Use the same pattern as _change_figure_selectors281            self._discard_figure_update = True282            self.normalize_xrf = False283            self._discard_figure_update = False284        285        # Continue with normal selector update logic286        self._update_selectors()287 288    @time_me()289    @param.depends(290        "mass_spec_selector",291        "gamma_ray_selector",292        "drilling_selector",293        watch=True,294    )295    def _update_selectors(self) -> None:296        self._update_figures_impl()297 298    def _update_figures_impl(self) -> None:299        if self._discard_figure_update:300            return301 302        fig_data = self._slctr_manager.get_curve_group()303        self._figure_manager.set_figure(self.fig_name, fig_data)304        self._app.trigger(CombCtrlUpdated())305 306    def _update_fig_dropdown(self) -> None:307        self._remove_fig_dropdown_button.items = self.param.fig_name.objects308 309    def _reset(self) -> None:310        self._figure_manager.set_figure(self.fig_name, {})311        empty_group = self._group_manager.get_curve_group("empty")312        self._slctr_manager.set_curve_group(empty_group)313 314    @time_me()315    def handle_normalization_viz_update(self, _event) -> None:316        """Trigger UpdateVizColumns for ALL normalized columns across all figures."""317        318        curves_to_add = []319        320        # Check all figures for normalized columns321        for fig_name, fig_curves in self.figures.items():322            # Skip if this figure doesn't have normalization enabled323            if not self._figure_manager.get_normalization_state(fig_name):324                continue325                326            xrf_curves = fig_curves.get(DatasetType.XRF, [])327            element_names = [curve.name for curve in xrf_curves] if xrf_curves else []328            329            if len(element_names) <= 1:330                continue331                332            # Get normalized column names for this figure333            fig_id = self._figure_manager.get_figure_id(fig_name)334            normalized_col_names = [f"{element}_norm_fig_{fig_id}" for element in element_names]335            336            # Add curves that exist in the registry337            for col_name in normalized_col_names:338                if col_name in self._app.curves.curves[DatasetType.XRF_STATS]:339                    curves_to_add.append(self._app.curves.curves[DatasetType.XRF_STATS][col_name])340        341        # Single UpdateVizColumns event for all normalized curves342        if curves_to_add:343            self._app.trigger(UpdateVizColumns(344                ctrl_name="NormalizationHandler",345                to_add=curves_to_add,346                to_remove=[]347            ))348 349    def recompute_active_normalizations(self, _event) -> None:350        """Recompute all active normalized columns after XRF data changes (e.g., slicing)."""351        normalizations = []352        353        # Iterate through all figures and collect normalization info354        for fig_name in self.figures.keys():355            is_normalized = self._figure_manager.get_normalization_state(fig_name)356            357            if not is_normalized:358                continue359                360            # Get XRF curves for this figure361            fig_curves = self.figures[fig_name]362            xrf_curves = fig_curves.get(DatasetType.XRF, [])363            element_names = [curve.name for curve in xrf_curves] if xrf_curves else []364            365            if not element_names or len(element_names) <= 1:366                continue367                368            # Get figure ID and add to normalizations list369            fig_id = self._figure_manager.get_figure_id(fig_name)370            normalizations.append(FigureNormalization(371                fig_id=fig_id,372                fig_name=fig_name,373                elements=element_names,374                normalize_enabled=True375            ))376        377        # Trigger single vectorized NormalizationUpdated event378        if normalizations:379            self._app.trigger(NormalizationUpdated(normalizations=normalizations))380 381 382class _CurveGroupManager:383    def __init__(self, ctrl: CombCurveController) -> None:384        self._ctrl = ctrl385        self._curve_groups = DEFAULT_CURVE_GROUPS.copy()386        self._groupnames = _NameGenerator("group", seed=2)387        self._group_name: str | None = None388 389    @property390    def group_names(self) -> list[str]:391        return list(self._curve_groups.keys())392 393    @property394    def group_name(self) -> str:395        assert self._group_name is not None396        return self._group_name397 398    @property399    def curve_group(self) -> TCurveGroup:400        return self._curve_groups[self.group_name]401 402    @property403    def is_group_name_set(self) -> bool:404        return self._group_name is not None405 406    def add_new_group(self, curve_group: TCurveGroup) -> None:407        group_name = self._groupnames.next()408        self._curve_groups[group_name] = curve_group409        self._group_name = group_name410 411    def update_group_name(self, group_name: str) -> None:412        self._group_name = group_name413 414    def get_curve_group(self, group_name: str) -> TCurveGroup:415        return self._curve_groups[group_name]416 417 418class _FigureManager:419 420    def __init__(421        self,422        ctrl: CombCurveController,423    ) -> None:424        self._ctrl = ctrl425        self._fignames = _NameGenerator("Chart")426        427        # Stable figure ID tracking428        self._figure_ids: dict[str, int] = {}  # fig_name -> stable_id429        self._id_to_name: dict[int, str] = {}  # stable_id -> current_name430        431        # Initialize with empty dictionaries - figures will be created on first file load432        self._empty_figures: dict[str, TCurveGroup] = {}433        self._figures: dict[str, TCurveGroup] = {}434        self._exportable_figures: dict[str, TExportCurveGroup] = {}435        self._normalization_states: dict[str, bool] = {}436        437        with param.discard_events(ctrl):438            # Set initial fig_name to None - will be set when first figure is created439            self.fig_name = None440            self.editable_fig_name = ""441        self._curve_group_converter = CurveGroupConverter(ctrl)442 443    @property444    def _loaded_curves(self) -> TLoadedCurves:445        return self._ctrl._loaded_curves446 447    @property448    def figures(self) -> dict[str, TCurveGroup]:449        return self._figures450 451    @property452    def exportable_figures(self) -> dict[str, TExportCurveGroup]:453        return self._exportable_figures454 455    @property456    def fig_name(self) -> str:457        return self._ctrl.fig_name458 459    @fig_name.setter460    def fig_name(self, value: str) -> None:461        self._ctrl.fig_name = value462 463    @property464    def fig_names(self) -> list[str]:465        return self._ctrl.param.fig_name.objects466 467    @fig_names.setter468    def fig_names(self, values: list[str]) -> None:469        self._ctrl.param.fig_name.objects = values470 471    @property472    def editable_fig_name(self) -> str:473        return self._ctrl.editable_fig_name474 475    @editable_fig_name.setter476    def editable_fig_name(self, value) -> None:477        self._ctrl.editable_fig_name = value478 479    def _sync_exportable(self, fig_name: str) -> None:480        curve_group = self._figures[fig_name]481        exportable_groups = self._curve_group_converter.convert(curve_group)482        self._exportable_figures[fig_name] = exportable_groups483 484    def _sync_fig_names(self) -> None:485        new_names = list(self._figures.keys())486        self.fig_names = new_names487 488    def set_figure(self, fig_name: str, curve_group: TCurveGroup, auto_trigger_viz: bool = True) -> None:489        if auto_trigger_viz:490            with _VizCurveUpdateContext(self._ctrl):491                self._figures[fig_name] = curve_group492                self._sync_exportable(fig_name)493        else:494            self._figures[fig_name] = curve_group495            self._sync_exportable(fig_name)496 497    def get_figure(self, fig_name: str) -> TCurveGroup:498        return self._figures[fig_name]499    500    def get_normalization_state(self, fig_name: str) -> bool:501        return self._normalization_states[fig_name]502    503    def set_normalization_state(self, fig_name: str, normalize: bool) -> None:504        self._normalization_states[fig_name] = normalize505    506    def get_figure_id(self, fig_name: str) -> int:507        """Get the stable ID for a figure by name."""508        return self._figure_ids[fig_name]509    510    def get_figure_name_by_id(self, fig_id: int) -> str:511        """Get the current name for a figure by its stable ID."""512        return self._id_to_name[fig_id]513 514    def update_editable(self) -> None:515        self.editable_fig_name = self.fig_name516 517    def save_editable_name(self) -> None:518        old_name, new_name = self.fig_name, self.editable_fig_name519 520        # Update ID mappings to reflect name change (ID stays the same)521        fig_id = self._figure_ids[old_name]522        self._figure_ids[new_name] = fig_id523        self._id_to_name[fig_id] = new_name524        del self._figure_ids[old_name]525 526        with _VizCurveUpdateContext(self._ctrl):527            fig = self._figures[old_name]528            self._figures[new_name] = fig529            del self._figures[old_name]530 531            exportable_fig = self._exportable_figures[old_name]532            self._exportable_figures[new_name] = exportable_fig533            del self._exportable_figures[old_name]534            535            norm_state = self._normalization_states[old_name]536            self._normalization_states[new_name] = norm_state537            del self._normalization_states[old_name]538 539        self._sync_fig_names()540        self.fig_name = new_name541 542    def is_editable_fig_name_new(self) -> bool:543        if self.editable_fig_name == self.fig_name:544            return False545 546        if self.editable_fig_name in self.fig_names:547            return False548 549        return True550 551    def add_new_figure(self, fig_name: str = None, curves: TCurveGroup = None, auto_trigger_viz: bool = True) -> None:552        if fig_name is None:553            # Current behavior: generate name and copy current figure554            new_fig_name = self._fignames.next()555            new_curves = {556                dataset_type: data557                for dataset_type, data in self._figures[self.fig_name].items()558            }559        else:560            # New behavior: use provided name and curves, but still call next() for ID management561            self._fignames.next()  # Advance the ID counter562            new_fig_name = fig_name563            new_curves = curves if curves is not None else {}564        565        new_fig_id = self._fignames.current_id()566        567        # Update ID mappings568        self._figure_ids[new_fig_name] = new_fig_id569        self._id_to_name[new_fig_id] = new_fig_name570        571        if auto_trigger_viz:572            with _VizCurveUpdateContext(self._ctrl):573                self._figures[new_fig_name] = new_curves574                self._sync_exportable(new_fig_name)575                self._normalization_states[new_fig_name] = False  # Always start new figures with normalization disabled576        else:577            self._figures[new_fig_name] = new_curves578            self._sync_exportable(new_fig_name)579            self._normalization_states[new_fig_name] = False  # Always start new figures with normalization disabled580            581        self._sync_fig_names()582        if fig_name is None:  # Only set as active figure if this was a user-initiated add583            self.fig_name = new_fig_name584 585    def remove_fig(self, forsaken_fig_name: str) -> None:586        remaining_figs = [fig for fig in self._figures if fig != forsaken_fig_name]587 588        if len(remaining_figs) == 0:589            self._ctrl._reset()590            return591 592        if forsaken_fig_name == self.fig_name:593            self.fig_name = remaining_figs[0]594 595        # Clean up ID mappings596        forsaken_id = self._figure_ids[forsaken_fig_name]597        del self._figure_ids[forsaken_fig_name]598        del self._id_to_name[forsaken_id]599 600        with _VizCurveUpdateContext(self._ctrl):601            del self._figures[forsaken_fig_name]602            del self._exportable_figures[forsaken_fig_name]603            del self._normalization_states[forsaken_fig_name]604 605        self.fig_names = remaining_figs606 607 608class CurveGroupConverter:609 610    def __init__(self, ctrl: CombCurveController) -> None:611        self._ctrl = ctrl612 613    @property614    def _loaded_curves(self) -> TLoadedCurves:615        return self._ctrl._loaded_curves616 617    def convert(self, curve_group: TCurveGroup) -> TExportCurveGroup:618        converted = {619            dataset_type: [620                self._loaded_curves[dataset_type][curve_name]621                for curve_name in curve_names622                if dataset_type in self._loaded_curves623                and curve_name in self._loaded_curves[dataset_type]624            ]625            for dataset_type, curve_names in curve_group.items()626        }627        return converted628 629 630@dataclass631class _SelectorMeta:632    parameter: param.Parameter633    value_getter: Callable[[], list[str] | None]634    dataset_type: DatasetType635 636 637class _SelectorManager:638 639    def __init__(640        self, ctrl: CombCurveController, selectors: list[_SelectorMeta]641    ) -> None:642        self._ctrl = ctrl643        self._selectors: dict[DatasetType, Callable[[], list[str] | None]] = {644            selector.dataset_type: selector.value_getter for selector in selectors645        }646        self._selector_params: dict[DatasetType, param.Parameter] = {647            selector.dataset_type: selector.parameter for selector in selectors648        }649 650    def sync_selector(self, dataset_type: DatasetType) -> None:651        p = self.get_param(dataset_type)652        objs = list(sorted(self._ctrl._loaded_curves[dataset_type].keys()))653        p.objects = objs654 655    def get_curve_group_params(self, curve_group: TCurveGroup) -> dict[str, list[str]]:656        """Get parameter data for a curve group without applying it."""657        return {658            parameter.name: curve_group.get(dataset_type, [])659            for dataset_type, parameter in self._selector_params.items()660        }661 662    def set_curve_group(663        self, curve_group: TCurveGroup, manual_trigger: bool = False664    ) -> None:665        new_selector_data = self.get_curve_group_params(curve_group)666        self._ctrl.param.update(**new_selector_data)667 668        if not manual_trigger:669            return670 671        for parameter_name in new_selector_data:672            self._ctrl.param.trigger(parameter_name)673 674    def get_curve_group(self) -> TCurveGroup:675        return {676            dataset_type: [677                curve_name678                for curve_name in (selector() or [])679                if curve_name in self._ctrl._loaded_curves[dataset_type]680            ]681            for dataset_type, selector in self._selectors.items()682        }683 684    def get_param(self, dataset_type: DatasetType) -> param.Parameter:685        return self._selector_params[dataset_type]686 687 688class _VizCurveUpdateContext:689    """690    Manages sending viz update events after updates to the combined691    curve view.692 693    Putting it in its own Context Manager made the code inside the controller694    easier to read.695 696    When the context is entered using697 698    ```699    with _VizCurveUpdateContext(self):700        ...701    ```702 703    then the current set of visible curves is stored. When the context is exited,704    the current set of visible curves is re-computed and the set differences are705    sent as an event to the CDS managers.706    """707 708    def __init__(self, ctrl: CombCurveController):709        self._app = ctrl._app710        self._name = ctrl.name711        self._figure_manager = ctrl._figure_manager712        self._old_curves: set[Curve] = set()713 714    def __enter__(self) -> None:715        self._old_curves = self._get_all_curves()716 717    def __exit__(self, *args, **kwargs) -> None:718        new_curves = self._get_all_curves()719        self._diff_trigger(self._old_curves, new_curves)720 721    def _diff_trigger(self, old_curves: set[Curve], new_curves: set[Curve]) -> None:722        to_add = new_curves.difference(old_curves)723        to_remove = old_curves.difference(new_curves)724 725        if len(to_add) == 0 and len(to_remove) == 0:726            return727 728        event = UpdateVizColumns(729            ctrl_name=self._name,730            to_add=list(to_add),731            to_remove=list(to_remove),732        )733        self._app.trigger(event)734 735    def _get_all_curves(self) -> set[Curve]:736        # TODO: BUG WHEN CURVE NAMES ARE EQUAL ACROSS DATASETS737        figures = self._figure_manager.exportable_figures738        return {739            curve740            for chart in figures741            for dataset_type in figures[chart]742            for curve in (figures[chart][dataset_type] or [])743        }744 745 746class _NameGenerator:747 748    def __init__(self, prefix: str, seed: int = 0) -> None:749        self._prefix = prefix750        self._i: int = seed751 752    def next(self) -> str:753        self._i += 1754        return f"{self._prefix} {self._i}"755    756    def current_id(self) -> int:757        """Get the current counter value (the ID of the most recently created item)."""758        return self._i759