From e56c962322bcbda2e002fbad47be8d6f2cca18f0 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 05:40:06 -0400 Subject: [PATCH 1/8] add References object to PlotArea, other cleanup --- fastplotlib/graphics/_base.py | 5 +- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 2 +- .../graphics/selectors/_base_selector.py | 4 +- fastplotlib/graphics/selectors/_linear.py | 8 +- fastplotlib/graphics/selectors/_polygon.py | 2 +- fastplotlib/layouts/_plot_area.py | 183 ++++++++++-------- fastplotlib/widgets/histogram_lut.py | 10 +- 8 files changed, 121 insertions(+), 95 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 4442c851e..15ac601a5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -166,6 +166,9 @@ def children(self) -> list[WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children + def _fpl_add_plot_area_hook(self, plot_area): + self._plot_area = plot_area + def __setattr__(self, key, value): if hasattr(self, key): attr = getattr(self, key) @@ -192,7 +195,7 @@ def __eq__(self, other): return False - def _cleanup(self): + def _fpl_cleanup(self): """ Cleans up the graphic in preparation for __del__(), such as removing event handlers from plot renderer, feature event handlers, etc. diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f44347a58..cfb697dff 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -278,7 +278,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): return bounds_init, limits, size, origin, axis, end_points - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def set_feature(self, feature: str, new_data: Any, indices: Any = None): diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 8488ec15e..e811468b6 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -416,7 +416,7 @@ def _get_linear_selector_init_args(self, padding, **kwargs): return bounds, limits, size, origin, axis, end_points - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def set_feature(self, feature: str, new_data: Any, indices: Any): diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 6c1f8c6ae..f3cc62cc9 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -123,7 +123,7 @@ def _get_source(self, graphic): return source - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area # when the pointer is pressed on a fill, edge or vertex @@ -356,7 +356,7 @@ def _key_up(self, ev): self._move_info = None - def _cleanup(self): + def _fpl_cleanup(self): """ Cleanup plot renderer event handlers etc. """ diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 886ccbaaf..307b276d9 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -208,8 +208,8 @@ def _ipywidget_callback(self, change): self.selection = change["new"] - def _add_plot_area_hook(self, plot_area): - super()._add_plot_area_hook(plot_area=plot_area) + def _fpl_add_plot_area_hook(self, plot_area): + super()._fpl_add_plot_area_hook(plot_area=plot_area) # resize the slider widgets when the canvas is resized self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") @@ -375,8 +375,8 @@ def _move_graphic(self, delta: np.ndarray): else: self.selection = self.selection() + delta[1] - def _cleanup(self): - super()._cleanup() + def _fpl_cleanup(self): + super()._fpl_cleanup() for widget in self._handled_widgets: widget.unobserve(self._ipywidget_callback, "value") diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 44d378329..3d2ee98fd 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -39,7 +39,7 @@ def get_vertices(self) -> np.ndarray: return np.vstack(vertices) - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area # click to add new segment diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 299bc6e5d..440a884e1 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -14,12 +14,67 @@ from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend -# dict to store Graphic instances -# this is the only place where the real references to Graphics are stored in a Python session -# {hex id str: Graphic} + HexStr: TypeAlias = str -GRAPHICS: dict[HexStr, Graphic] = dict() -SELECTORS: dict[HexStr, BaseSelector] = dict() + + +class References: + """ + This is the only place where the real graphic objects are stored. Everywhere else gets a proxy. + """ + _graphics: dict[HexStr, Graphic] = dict() + _selectors: dict[HexStr, BaseSelector] = dict() + _legends: dict[HexStr, Legend] = dict() + + def add(self, graphic: Graphic | BaseSelector | Legend): + """Adds the real graphic to the dict""" + loc = graphic.loc + + if isinstance(graphic, BaseSelector): + self._selectors[loc] = graphic + + elif isinstance(graphic, Legend): + self._legends[loc] = graphic + + elif isinstance(graphic, Graphic): + self._graphics[loc] = graphic + + else: + raise TypeError("Can only add Graphic, Selector or Legend types") + + def remove(self, address): + if address in self._graphics.keys(): + del self._graphics[address] + elif address in self._selectors.keys(): + del self._selectors[address] + elif address in self._legends.keys(): + del self._legends[address] + else: + raise KeyError( + f"graphic with address not found: {address}" + ) + + def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: + proxies = list() + for key in refs: + if key in self._graphics.keys(): + proxies.append(weakref.proxy(self._graphics[key])) + + elif key in self._selectors.keys(): + proxies.append(weakref.proxy(self._selectors[key])) + + elif key in self._legends.keys(): + proxies.append(weakref.proxy(self._legends[key])) + + else: + raise KeyError( + f"graphic object with address not found: {key}" + ) + + return tuple(proxies) + + +REFERENCES = References() class PlotArea: @@ -89,12 +144,15 @@ def __init__( self.renderer.add_event_handler(self.set_viewport_rect, "resize") # list of hex id strings for all graphics managed by this PlotArea - # the real Graphic instances are stored in the ``GRAPHICS`` dict - self._graphics: list[str] = list() + # the real Graphic instances are managed by REFERENCES + self._graphics: list[HexStr] = list() # selectors are in their own list so they can be excluded from scene bbox calculations # managed similar to GRAPHICS for garbage collection etc. - self._selectors: list[str] = list() + self._selectors: list[HexStr] = list() + + # legends, managed just like other graphics as explained above + self._legends: list[HexStr] = list() self._name = name @@ -206,35 +264,17 @@ def controller(self, new_controller: str | pygfx.Controller): @property def graphics(self) -> tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" - proxies = list() - for loc in self._graphics: - p = weakref.proxy(GRAPHICS[loc]) - if p.__class__.__name__ == "Legend": - continue - proxies.append(p) - - return tuple(proxies) + return REFERENCES.get_proxies(self._graphics) @property def selectors(self) -> tuple[BaseSelector, ...]: """Selectors in the plot area. Always returns a proxy to the Graphic instances.""" - proxies = list() - for loc in self._selectors: - p = weakref.proxy(SELECTORS[loc]) - proxies.append(p) - - return tuple(proxies) + return REFERENCES.get_proxies(self._selectors) @property def legends(self) -> tuple[Legend, ...]: """Legends in the plot area.""" - proxies = list() - for loc in self._graphics: - p = weakref.proxy(GRAPHICS[loc]) - if p.__class__.__name__ == "Legend": - proxies.append(p) - - return tuple(proxies) + return REFERENCES.get_proxies(self._legends) @property def name(self) -> str: @@ -470,28 +510,28 @@ def _add_or_insert_graphic( if graphic.name is not None: # skip for those that have no name self._check_graphic_name_exists(graphic.name) + loc = graphic.loc + if isinstance(graphic, BaseSelector): - # store in SELECTORS dict - loc = graphic.loc - SELECTORS[loc] = ( - graphic # add hex id string for referencing this graphic instance - ) - # don't manage garbage collection of LineSliders for now - if action == "insert": - self._selectors.insert(index, loc) - else: - self._selectors.append(loc) + loc_list = getattr(self, "_selectors") + + elif isinstance(graphic, Legend): + loc_list = getattr(self, "_legends") + + elif isinstance(graphic, Graphic): + loc_list = getattr(self, "_graphics") + else: - # store in GRAPHICS dict - loc = graphic.loc - GRAPHICS[loc] = ( - graphic # add hex id string for referencing this graphic instance - ) + raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") - if action == "insert": - self._graphics.insert(index, loc) - else: - self._graphics.append(loc) + if action == "insert": + loc_list.insert(index, loc) + elif action == "add": + loc_list.append(loc) + else: + raise ValueError("valid actions are 'insert' | 'add'") + + REFERENCES.add(graphic) # now that it's in the dict, just use the weakref graphic = weakref.proxy(graphic) @@ -503,24 +543,13 @@ def _add_or_insert_graphic( self.center_graphic(graphic) # if we don't use the weakref above, then the object lingers if a plot hook is used! - if hasattr(graphic, "_add_plot_area_hook"): - graphic._add_plot_area_hook(self) + graphic._fpl_add_plot_area_hook(self) def _check_graphic_name_exists(self, name): - graphic_names = list() - - for g in self.graphics: - graphic_names.append(g.name) - - for s in self.selectors: - graphic_names.append(s.name) - - for l in self.legends: - graphic_names.append(l.name) - - if name in graphic_names: + if name in self: raise ValueError( - f"graphics must have unique names, current graphic names are:\n {graphic_names}" + f"Graphic with given name already exists in subplot or plot area. " + f"All graphics within a subplot or plot area must have a unique name." ) def center_graphic(self, graphic: Graphic, zoom: float = 1.35): @@ -649,35 +678,29 @@ def delete_graphic(self, graphic: Graphic): # TODO: proper gc of selectors, RAM is freed for regular graphics but not selectors # TODO: references to selectors must be lingering somewhere # TODO: update March 2024, I think selectors are gc properly, should check - # get location - loc = graphic.loc + # get memory address + address = graphic.loc # check which dict it's in - if loc in self._graphics: - glist = self._graphics - kind = "graphic" - elif loc in self._selectors: - kind = "selector" - glist = self._selectors + if address in self._graphics: + self._graphics.remove(address) + elif address in self._selectors: + self._selectors.remove(address) + elif address in self._legends: + self._legends.remove(address) else: raise KeyError( - f"Graphic with following address not found in plot area: {loc}" + f"Graphic with following address not found in plot area: {address}" ) # remove from scene if necessary if graphic.world_object in self.scene.children: self.scene.remove(graphic.world_object) - # remove from list of addresses - glist.remove(loc) - # cleanup - graphic._cleanup() + graphic._fpl_cleanup() - if kind == "graphic": - del GRAPHICS[loc] - elif kind == "selector": - del SELECTORS[loc] + REFERENCES.remove(address) def clear(self): """ diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 43f2b48b3..7a5f1d6cf 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -124,10 +124,10 @@ def _get_vmin_vmax_str(self) -> tuple[str, str]: return vmin_str, vmax_str - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area - self.linear_region._add_plot_area_hook(plot_area) - self.line._add_plot_area_hook(plot_area) + self.linear_region._fpl_add_plot_area_hook(plot_area) + self.line._fpl_add_plot_area_hook(plot_area) self._plot_area.auto_scale() @@ -296,7 +296,7 @@ def image_graphic(self, graphic): self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) - def _cleanup(self): - self.linear_region._cleanup() + def _fpl_cleanup(self): + self.linear_region._fpl_cleanup() del self.line del self.linear_region From 7be74075322be6d40b15658ef90a7bc4339038c1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 06:35:21 -0400 Subject: [PATCH 2/8] more cleanup --- fastplotlib/graphics/_base.py | 36 +++++---- fastplotlib/graphics/selectors/_linear.py | 20 ++--- .../graphics/selectors/_linear_region.py | 7 +- fastplotlib/layouts/_plot_area.py | 79 +++++++------------ fastplotlib/legends/legend.py | 12 +-- 5 files changed, 67 insertions(+), 87 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 15ac601a5..e406f4307 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,4 +1,4 @@ -from typing import Any, Literal +from typing import Any, Literal, TypeAlias import weakref from warnings import warn from abc import ABC, abstractmethod @@ -11,9 +11,12 @@ from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted + +HexStr: TypeAlias = str + # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: dict[str, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[HexStr, WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ @@ -80,7 +83,7 @@ def __init__( self.present = PresentFeature(parent=self) # store hex id str of Graphic instance mem location - self.loc: str = hex(id(self)) + self._fpl_address: HexStr = hex(id(self)) self.deleted = Deleted(self, False) @@ -102,10 +105,10 @@ def name(self, name: str): def world_object(self) -> WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" # We use weakref to simplify garbage collection - return weakref.proxy(WORLD_OBJECTS[hex(id(self))]) + return weakref.proxy(WORLD_OBJECTS[self._fpl_address]) def _set_world_object(self, wo: WorldObject): - WORLD_OBJECTS[hex(id(self))] = wo + WORLD_OBJECTS[self._fpl_address] = wo @property def position(self) -> np.ndarray: @@ -190,7 +193,7 @@ def __eq__(self, other): if not isinstance(other, Graphic): raise TypeError("`==` operator is only valid between two Graphics") - if self.loc == other.loc: + if self._fpl_address == other._fpl_address: return True return False @@ -206,7 +209,7 @@ def _fpl_cleanup(self): def __del__(self): self.deleted = True - del WORLD_OBJECTS[self.loc] + del WORLD_OBJECTS[self._fpl_address] def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): """Rotate the Graphic with respect to the world. @@ -375,7 +378,7 @@ def _event_handler(self, event): else: # get index of world object that made this event for i, item in enumerate(self.graphics): - wo = WORLD_OBJECTS[item.loc] + wo = WORLD_OBJECTS[item._fpl_address] # we only store hex id of worldobject, but worldobject `pick_info` is always the real object # so if pygfx worldobject triggers an event by itself, such as `click`, etc., this will be # the real world object in the pick_info and not the proxy @@ -435,7 +438,8 @@ class PreviouslyModifiedData: indices: Any -COLLECTION_GRAPHICS: dict[str, Graphic] = dict() +# Dict that holds all collection graphics in one python instance +COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() class GraphicCollection(Graphic): @@ -453,7 +457,7 @@ def graphics(self) -> np.ndarray[Graphic]: """The Graphics within this collection. Always returns a proxy to the Graphics.""" if self._graphics_changed: proxies = [ - weakref.proxy(COLLECTION_GRAPHICS[loc]) for loc in self._graphics + weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics ] self._graphics_array = np.array(proxies) self._graphics_array.flags["WRITEABLE"] = False @@ -482,10 +486,10 @@ def add_graphic(self, graphic: Graphic, reset_index: False): f"you are trying to add a {graphic.__class__.__name__}." ) - loc = hex(id(graphic)) - COLLECTION_GRAPHICS[loc] = graphic + addr = graphic._fpl_address + COLLECTION_GRAPHICS[addr] = graphic - self._graphics.append(loc) + self._graphics.append(addr) if reset_index: self._reset_index() @@ -510,7 +514,7 @@ def remove_graphic(self, graphic: Graphic, reset_index: True): """ - self._graphics.remove(graphic.loc) + self._graphics.remove(graphic._fpl_address) if reset_index: self._reset_index() @@ -528,8 +532,8 @@ def __getitem__(self, key): def __del__(self): self.world_object.clear() - for loc in self._graphics: - del COLLECTION_GRAPHICS[loc] + for addr in self._graphics: + del COLLECTION_GRAPHICS[addr] super().__del__() diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 307b276d9..99fc02936 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -140,18 +140,6 @@ def __init__( world_object.add(self.line_outer) world_object.add(line_inner) - self._set_world_object(world_object) - - # set x or y position - if axis == "x": - self.position_x = selection - else: - self.position_y = selection - - self.selection = LinearSelectionFeature( - self, axis=axis, value=selection, limits=self._limits - ) - self._move_info: dict = None self.parent = parent @@ -170,6 +158,14 @@ def __init__( name=name, ) + self._set_world_object(world_object) + + self.selection = LinearSelectionFeature( + self, axis=axis, value=selection, limits=self._limits + ) + + self.selection = selection + def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector value = self.selection() diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index b88174ddb..47191bfb1 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -135,7 +135,6 @@ def __init__( # basic mesh for the fill area of the selector # line for each edge of the selector group = pygfx.Group() - self._set_world_object(group) if axis == "x": mesh = pygfx.Mesh( @@ -155,7 +154,7 @@ def __init__( self.fill = mesh self.fill.world.position = (*origin, -2) - self.world_object.add(self.fill) + group.add(self.fill) self._resizable = resizable @@ -223,7 +222,7 @@ def __init__( # add the edge lines for edge in self.edges: edge.world.z = -1 - self.world_object.add(edge) + group.add(edge) # set the initial bounds of the selector self.selection = LinearRegionSelectionFeature( @@ -244,6 +243,8 @@ def __init__( name=name, ) + self._set_world_object(group) + def get_selected_data( self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray], None]: diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 440a884e1..9b4b69b51 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -28,16 +28,16 @@ class References: def add(self, graphic: Graphic | BaseSelector | Legend): """Adds the real graphic to the dict""" - loc = graphic.loc + addr = graphic._fpl_address if isinstance(graphic, BaseSelector): - self._selectors[loc] = graphic + self._selectors[addr] = graphic elif isinstance(graphic, Legend): - self._legends[loc] = graphic + self._legends[addr] = graphic elif isinstance(graphic, Graphic): - self._graphics[loc] = graphic + self._graphics[addr] = graphic else: raise TypeError("Can only add Graphic, Selector or Legend types") @@ -276,6 +276,10 @@ def legends(self) -> tuple[Legend, ...]: """Legends in the plot area.""" return REFERENCES.get_proxies(self._legends) + @property + def objects(self) -> tuple[Graphic | BaseSelector | Legend, ...]: + return *self.graphics, *self.selectors, *self.legends + @property def name(self) -> str: """The name of this plot area""" @@ -510,24 +514,24 @@ def _add_or_insert_graphic( if graphic.name is not None: # skip for those that have no name self._check_graphic_name_exists(graphic.name) - loc = graphic.loc + addr = graphic._fpl_address if isinstance(graphic, BaseSelector): - loc_list = getattr(self, "_selectors") + addr_list = self._selectors elif isinstance(graphic, Legend): - loc_list = getattr(self, "_legends") + addr_list = self._legends elif isinstance(graphic, Graphic): - loc_list = getattr(self, "_graphics") + addr_list = self._graphics else: raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") if action == "insert": - loc_list.insert(index, loc) + addr_list.insert(index, addr) elif action == "add": - loc_list.append(loc) + addr_list.append(addr) else: raise ValueError("valid actions are 'insert' | 'add'") @@ -679,20 +683,19 @@ def delete_graphic(self, graphic: Graphic): # TODO: references to selectors must be lingering somewhere # TODO: update March 2024, I think selectors are gc properly, should check # get memory address - address = graphic.loc - - # check which dict it's in - if address in self._graphics: - self._graphics.remove(address) - elif address in self._selectors: - self._selectors.remove(address) - elif address in self._legends: - self._legends.remove(address) - else: + address = graphic._fpl_address + + if graphic not in self: raise KeyError( - f"Graphic with following address not found in plot area: {address}" + f"Graphic not found in plot area: {graphic}" ) + # check which type it is + for l in [self._graphics, self._selectors, self._legends]: + if address in l: + l.remove(address) + break + # remove from scene if necessary if graphic.world_object in self.scene.children: self.scene.remove(graphic.world_object) @@ -706,51 +709,27 @@ def clear(self): """ Clear the Plot or Subplot. Also performs garbage collection, i.e. runs ``delete_graphic`` on all graphics. """ - - for g in self.graphics: + for g in self.objects: self.delete_graphic(g) - for s in self.selectors: - self.delete_graphic(s) - def __getitem__(self, name: str): - for graphic in self.graphics: + for graphic in self.objects: if graphic.name == name: return graphic - for selector in self.selectors: - if selector.name == name: - return selector - - for legend in self.legends: - if legend.name == name: - return legend - - graphic_names = list() - for g in self.graphics: - graphic_names.append(g.name) - - selector_names = list() - for s in self.selectors: - selector_names.append(s.name) - raise IndexError( - f"No graphic or selector of given name.\n" - f"The current graphics are:\n {graphic_names}\n" - f"The current selectors are:\n {selector_names}" + f"No graphic or selector of given name in plot area.\n" ) def __contains__(self, item: str | Graphic): - to_check = [*self.graphics, *self.selectors, *self.legends] - if isinstance(item, Graphic): - if item in to_check: + if item in self.objects: return True else: return False elif isinstance(item, str): - for graphic in to_check: + for graphic in self.objects: # only check named graphics if graphic.name is None: continue diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index be90004aa..b7e55f321 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -163,7 +163,7 @@ def __init__( """ self._graphics: list[Graphic] = list() - # hex id of Graphic, i.e. graphic.loc are the keys + # hex id of Graphic, i.e. graphic._fpl_address are the keys self._items: OrderedDict[str:LegendItem] = OrderedDict() super().__init__(*args, **kwargs) @@ -218,7 +218,7 @@ def _check_label_unique(self, label): def add_graphic(self, graphic: Graphic, label: str = None): if graphic in self._graphics: raise KeyError( - f"Graphic already exists in legend with label: '{self._items[graphic.loc].label}'" + f"Graphic already exists in legend with label: '{self._items[graphic._fpl_address].label}'" ) self._check_label_unique(label) @@ -268,7 +268,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): self._reset_mesh_dims() self._graphics.append(graphic) - self._items[graphic.loc] = legend_item + self._items[graphic._fpl_address] = legend_item graphic.deleted.add_event_handler(partial(self.remove_graphic, graphic)) @@ -288,7 +288,7 @@ def _reset_mesh_dims(self): def remove_graphic(self, graphic: Graphic): self._graphics.remove(graphic) - legend_item = self._items.pop(graphic.loc) + legend_item = self._items.pop(graphic._fpl_address) self._legend_items_group.remove(legend_item.world_object) self._reset_item_positions() @@ -350,7 +350,7 @@ def __getitem__(self, graphic: Graphic) -> LegendItem: if not isinstance(graphic, Graphic): raise TypeError("Must index Legend with Graphics") - if graphic.loc not in self._items.keys(): + if graphic._fpl_address not in self._items.keys(): raise KeyError("Graphic not in legend") - return self._items[graphic.loc] + return self._items[graphic._fpl_address] From 8b82e89167919b43eb6e2666cd4a119cb681214d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 06:35:38 -0400 Subject: [PATCH 3/8] black --- fastplotlib/layouts/_plot_area.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 9b4b69b51..06d9a606f 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -22,6 +22,7 @@ class References: """ This is the only place where the real graphic objects are stored. Everywhere else gets a proxy. """ + _graphics: dict[HexStr, Graphic] = dict() _selectors: dict[HexStr, BaseSelector] = dict() _legends: dict[HexStr, Legend] = dict() @@ -50,9 +51,7 @@ def remove(self, address): elif address in self._legends.keys(): del self._legends[address] else: - raise KeyError( - f"graphic with address not found: {address}" - ) + raise KeyError(f"graphic with address not found: {address}") def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: proxies = list() @@ -67,9 +66,7 @@ def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: proxies.append(weakref.proxy(self._legends[key])) else: - raise KeyError( - f"graphic object with address not found: {key}" - ) + raise KeyError(f"graphic object with address not found: {key}") return tuple(proxies) @@ -686,9 +683,7 @@ def delete_graphic(self, graphic: Graphic): address = graphic._fpl_address if graphic not in self: - raise KeyError( - f"Graphic not found in plot area: {graphic}" - ) + raise KeyError(f"Graphic not found in plot area: {graphic}") # check which type it is for l in [self._graphics, self._selectors, self._legends]: @@ -717,9 +712,7 @@ def __getitem__(self, name: str): if graphic.name == name: return graphic - raise IndexError( - f"No graphic or selector of given name in plot area.\n" - ) + raise IndexError(f"No graphic or selector of given name in plot area.\n") def __contains__(self, item: str | Graphic): if isinstance(item, Graphic): From b26a70c2ca28cbfe36cf53ca4d34ca3e9824e533 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 07:44:14 -0400 Subject: [PATCH 4/8] add refcount utility to References, fix Graphic.name setter bug --- fastplotlib/layouts/_plot_area.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 06d9a606f..eda0a3a08 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -1,4 +1,5 @@ from inspect import getfullargspec +from sys import getrefcount from typing import TypeAlias, Literal, Union import weakref from warnings import warn @@ -70,11 +71,23 @@ def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: return tuple(proxies) + def get_refcounts(self) -> dict[HexStr: int]: + counts = dict() + + for item in (self._graphics, self._selectors, self._legends): + for k in item.keys(): + counts[(k, item[k].name, item[k].__class__.__name__)] = getrefcount(item[k]) + + return counts + REFERENCES = References() class PlotArea: + def get_refcounts(self): + return REFERENCES.get_refcounts() + def __init__( self, parent: Union["PlotArea", "GridPlot"], From ca435dfdc4cf3508e8002ed0aa038bcdd8cc103a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 08:34:36 -0400 Subject: [PATCH 5/8] actually fix Graphic.name setter bug --- fastplotlib/graphics/_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index e406f4307..57c3bfb5e 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -96,11 +96,17 @@ def name(self) -> str | None: @name.setter def name(self, name: str): + if self.name == name: + return + if not isinstance(name, str): raise TypeError("`Graphic` name must be of type ") + if self._plot_area is not None: self._plot_area._check_graphic_name_exists(name) + self._name = name + @property def world_object(self) -> WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" From 6a0264739b74ce26584b4104897ac7b6ef60df59 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 09:55:52 -0400 Subject: [PATCH 6/8] more generalized event handler cleanup in Graphic --- fastplotlib/graphics/_base.py | 31 ++++++++++++++++++- .../graphics/selectors/_base_selector.py | 29 ++--------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 57c3bfb5e..3a5b043f5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -211,7 +211,36 @@ def _fpl_cleanup(self): Optionally implemented in subclasses """ - pass + # clear any attached event handlers and animation functions + for attr in dir(self): + try: + method = getattr(self, attr) + except: + continue + + if not callable(method): + continue + + for ev_type in PYGFX_EVENTS: + try: + self._plot_area.renderer.remove_event_handler(method, ev_type) + except (KeyError, TypeError): + pass + + try: + self._plot_area.remove_animation(method) + except KeyError: + pass + + for child in self.world_object.children: + child._event_handlers.clear() + + self.world_object._event_handlers.clear() + + feature_names = getattr(self, "feature_events") + for n in feature_names: + fea = getattr(self, n) + fea.clear_event_handlers() def __del__(self): self.deleted = True diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index f3cc62cc9..feb3d42ad 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -1,6 +1,7 @@ from typing import * from dataclasses import dataclass from functools import partial +import weakref import numpy as np @@ -136,8 +137,8 @@ def _fpl_add_plot_area_hook(self, plot_area): for fill in self._fill: if fill.material.color_is_transparent: - pfunc_fill = partial(self._check_fill_pointer_event, fill) - self._plot_area.renderer.add_event_handler(pfunc_fill, "pointer_down") + self._pfunc_fill = partial(self._check_fill_pointer_event, fill) + self._plot_area.renderer.add_event_handler(self._pfunc_fill, "pointer_down") # when the pointer moves self._plot_area.renderer.add_event_handler(self._move, "pointer_move") @@ -355,27 +356,3 @@ def _key_up(self, ev): self._key_move_value = False self._move_info = None - - def _fpl_cleanup(self): - """ - Cleanup plot renderer event handlers etc. - """ - self._plot_area.renderer.remove_event_handler(self._move, "pointer_move") - self._plot_area.renderer.remove_event_handler(self._move_end, "pointer_up") - self._plot_area.renderer.remove_event_handler(self._move_to_pointer, "click") - - self._plot_area.renderer.remove_event_handler(self._key_down, "key_down") - self._plot_area.renderer.remove_event_handler(self._key_up, "key_up") - - # remove animation func - self._plot_area.remove_animation(self._key_hold) - - # clear wo event handlers - for wo in self._world_objects: - wo._event_handlers.clear() - - if hasattr(self, "feature_events"): - feature_names = getattr(self, "feature_events") - for n in feature_names: - fea = getattr(self, n) - fea.clear_event_handlers() From aed5fb1ccd6b3a6484be690d3f47ba605b7ab383 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 22:44:21 -0400 Subject: [PATCH 7/8] all gc works now --- examples/notebooks/test_gc.ipynb | 200 ++++++++++++++++++ fastplotlib/graphics/line_collection.py | 1 - .../graphics/selectors/_base_selector.py | 6 + fastplotlib/graphics/selectors/_linear.py | 4 +- fastplotlib/widgets/histogram_lut.py | 61 +++--- fastplotlib/widgets/image.py | 14 +- 6 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 examples/notebooks/test_gc.ipynb diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb new file mode 100644 index 000000000..6caf6a9e3 --- /dev/null +++ b/examples/notebooks/test_gc.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9dfba6cf-38af-4003-90b9-463c0cb1063f", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "import pytest" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7552eedc-3b9b-4682-8e3b-7d44e0e5510d", + "metadata": {}, + "outputs": [], + "source": [ + "def test_references(plot_objects):\n", + " for i in range(len(plot_objects)):\n", + " with pytest.raises(ReferenceError) as failure:\n", + " plot_objects[i]\n", + " pytest.fail(f\"GC failed for object: {objects[i]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "948108e8-a4fa-4dc7-9953-a956428128cf", + "metadata": {}, + "source": [ + "# Add graphics and selectors, add feature event handlers, test gc occurs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d96bf14-b484-455e-bcd7-5b2fe7b45fb4", + "metadata": {}, + "outputs": [], + "source": [ + "xs = np.linspace(0, 20 * np.pi, 1_000)\n", + "ys = np.sin(xs)\n", + "zs = np.zeros(xs.size)\n", + "\n", + "points_data = np.column_stack([xs, ys, zs])\n", + "\n", + "line_collection_data = [points_data[:, 1].copy() for i in range(10)]\n", + "\n", + "img_data = np.random.rand(2_000, 2_000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "284b83e1-8cfc-4105-b7c2-6214137dab31", + "metadata": {}, + "outputs": [], + "source": [ + "gp = fpl.GridPlot((2, 2))\n", + "\n", + "line = gp[0, 0].add_line(points_data, name=\"line\")\n", + "scatter = gp[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", + "line_stack = gp[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", + "image = gp[1, 1].add_image(img_data, name=\"image\")\n", + "\n", + "linear_sel = line.add_linear_selector(name=\"line_linear_sel\")\n", + "linear_region_sel = line.add_linear_region_selector(name=\"line_region_sel\")\n", + "\n", + "linear_sel2 = line_stack.add_linear_selector(name=\"line-stack_linear_sel\")\n", + "linear_region_sel2 = line_stack.add_linear_region_selector(name=\"line-stack_region_sel\")\n", + "\n", + "linear_sel_img = image.add_linear_selector(name=\"image_linear_sel\")\n", + "linear_region_sel_img = image.add_linear_region_selector(name=\"image_linear_region_sel\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb2083c1-f6b7-417c-86b8-9980819917db", + "metadata": {}, + "outputs": [], + "source": [ + "def feature_changed_handler(ev):\n", + " pass\n", + "\n", + "\n", + "objects = list()\n", + "for subplot in gp:\n", + " objects += subplot.objects\n", + "\n", + "\n", + "for g in objects:\n", + " for feature in g.feature_events:\n", + " if isinstance(g, fpl.LineCollection):\n", + " continue # skip collections for now\n", + " \n", + " f = getattr(g, feature)\n", + " f.add_event_handler(feature_changed_handler)\n", + "\n", + "gp.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba9fffeb-45bd-4a0c-a941-e7c7e68f2e55", + "metadata": {}, + "outputs": [], + "source": [ + "gp.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e33bf32d-b13a-474b-92ca-1d1e1c7b820b", + "metadata": {}, + "outputs": [], + "source": [ + "test_references(objects)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8078a7d2-9bc6-48a1-896c-7e169c5bbdcf", + "metadata": {}, + "outputs": [], + "source": [ + "movies = [np.random.rand(100, 100, 100) for i in range(6)]\n", + "\n", + "iw = fpl.ImageWidget(movies)\n", + "\n", + "# add some events onto all the image graphics\n", + "for g in iw.managed_graphics:\n", + " for f in g.feature_events:\n", + " fea = getattr(g, f)\n", + " fea.add_event_handler(feature_changed_handler)\n", + "\n", + "iw.show()" + ] + }, + { + "cell_type": "markdown", + "id": "189bcd7a-40a2-4e84-abcf-c334e50f5544", + "metadata": {}, + "source": [ + "# Test that setting new data with different dims clears old ImageGraphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38557b63-997f-433a-b744-e562e30be6ae", + "metadata": {}, + "outputs": [], + "source": [ + "old_graphics = iw.managed_graphics\n", + "\n", + "new_movies = [np.random.rand(100, 200, 200) for i in range(6)]\n", + "\n", + "iw.set_data(new_movies)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e3c193-5672-4a66-bdca-12f1dd675d32", + "metadata": {}, + "outputs": [], + "source": [ + "test_references(old_graphics)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index e811468b6..1c2e151e8 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -15,7 +15,6 @@ class LineCollection(GraphicCollection, Interaction): child_type = LineGraphic.__name__ - feature_events = {"data", "colors", "cmap", "thickness", "present"} def __init__( self, diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index feb3d42ad..93fa53081 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -356,3 +356,9 @@ def _key_up(self, ev): self._key_move_value = False self._move_info = None + + def _fpl_cleanup(self): + if hasattr(self, "_pfunc_fill"): + self._plot_area.renderer.remove_event_handler(self._pfunc_fill, "pointer_down") + del self._pfunc_fill + super()._fpl_cleanup() diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 99fc02936..4b77a6cd9 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -372,9 +372,7 @@ def _move_graphic(self, delta: np.ndarray): self.selection = self.selection() + delta[1] def _fpl_cleanup(self): - super()._fpl_cleanup() - for widget in self._handled_widgets: widget.unobserve(self._ipywidget_callback, "value") - self._plot_area.renderer.remove_event_handler(self._set_slider_layout, "resize") + super()._fpl_cleanup() diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 7a5f1d6cf..1e2fedb10 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -44,14 +44,14 @@ def __init__( line_data = np.column_stack([hist_scaled, edges_flanked]) - self.line = LineGraphic(line_data) + self._histogram_line = LineGraphic(line_data) bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-1]) size = 120 # since it's scaled to 100 origin = (hist_scaled.max() / 2, 0) - self.linear_region = LinearRegionSelector( + self._linear_region_selector = LinearRegionSelector( bounds=bounds, limits=limits, size=size, @@ -61,7 +61,7 @@ def __init__( ) # there will be a small difference with the histogram edges so this makes them both line up exactly - self.linear_region.selection = ( + self._linear_region_selector.selection = ( image_graphic.cmap.vmin, image_graphic.cmap.vmax, ) @@ -91,8 +91,8 @@ def __init__( widget_wo = Group() widget_wo.add( - self.line.world_object, - self.linear_region.world_object, + self._histogram_line.world_object, + self._linear_region_selector.world_object, self._text_vmin.world_object, self._text_vmax.world_object, ) @@ -102,12 +102,12 @@ def __init__( self.world_object.local.scale_x *= -1 self._text_vmin.position_x = -120 - self._text_vmin.position_y = self.linear_region.selection()[0] + self._text_vmin.position_y = self._linear_region_selector.selection()[0] self._text_vmax.position_x = -120 - self._text_vmax.position_y = self.linear_region.selection()[1] + self._text_vmax.position_y = self._linear_region_selector.selection()[1] - self.linear_region.selection.add_event_handler(self._linear_region_handler) + self._linear_region_selector.selection.add_event_handler(self._linear_region_handler) self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) @@ -126,8 +126,8 @@ def _get_vmin_vmax_str(self) -> tuple[str, str]: def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area - self.linear_region._fpl_add_plot_area_hook(plot_area) - self.line._fpl_add_plot_area_hook(plot_area) + self._linear_region_selector._fpl_add_plot_area_hook(plot_area) + self._histogram_line._fpl_add_plot_area_hook(plot_area) self._plot_area.auto_scale() @@ -192,7 +192,7 @@ def _calculate_histogram(self, data): def _linear_region_handler(self, ev): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - vmin, vmax = self.linear_region.selection() + vmin, vmax = self._linear_region_selector.selection() vmin, vmax = vmin / self._scale_factor, vmax / self._scale_factor self.vmin, self.vmax = vmin, vmax @@ -201,7 +201,7 @@ def _image_cmap_handler(self, ev): def _block_events(self, b: bool): self.image_graphic.cmap.block_events(b) - self.linear_region.selection.block_events(b) + self._linear_region_selector.selection.block_events(b) @property def vmin(self) -> float: @@ -213,9 +213,9 @@ def vmin(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = ( + self._linear_region_selector.selection = ( value * self._scale_factor, - self.linear_region.selection()[1], + self._linear_region_selector.selection()[1], ) self.image_graphic.cmap.vmin = value @@ -224,7 +224,7 @@ def vmin(self, value: float): self._vmin = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmin.position_y = self.linear_region.selection()[0] + self._text_vmin.position_y = self._linear_region_selector.selection()[0] self._text_vmin.text = vmin_str @property @@ -237,8 +237,8 @@ def vmax(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = ( - self.linear_region.selection()[0], + self._linear_region_selector.selection = ( + self._linear_region_selector.selection()[0], value * self._scale_factor, ) self.image_graphic.cmap.vmax = value @@ -248,7 +248,7 @@ def vmax(self, value: float): self._vmax = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmax.position_y = self.linear_region.selection()[1] + self._text_vmax.position_y = self._linear_region_selector.selection()[1] self._text_vmax.text = vmax_str def set_data(self, data, reset_vmin_vmax: bool = True): @@ -256,7 +256,7 @@ def set_data(self, data, reset_vmin_vmax: bool = True): line_data = np.column_stack([hist_scaled, edges_flanked]) - self.line.data = line_data + self._histogram_line.data = line_data bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-11]) @@ -265,12 +265,12 @@ def set_data(self, data, reset_vmin_vmax: bool = True): if reset_vmin_vmax: # reset according to the new data - self.linear_region.limits = limits - self.linear_region.selection = bounds + self._linear_region_selector.limits = limits + self._linear_region_selector.selection = bounds else: # don't change the current selection self._block_events(True) - self.linear_region.limits = limits + self._linear_region_selector.limits = limits self._block_events(False) self._data = weakref.proxy(data) @@ -289,14 +289,21 @@ def image_graphic(self, graphic): f"HistogramLUT can only use ImageGraphic types, you have passed: {type(graphic)}" ) - # cleanup events from current image graphic - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + if self._image_graphic is not None: + # cleanup events from current image graphic + self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) self._image_graphic = graphic self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) + def disconnect_image_graphic(self): + self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + del self._image_graphic + # self._image_graphic = None + def _fpl_cleanup(self): - self.linear_region._fpl_cleanup() - del self.line - del self.linear_region + self._linear_region_selector._fpl_cleanup() + self._histogram_line._fpl_cleanup() + del self._histogram_line + del self._linear_region_selector diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 9412f7cc5..acef26a7d 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -875,17 +875,23 @@ def set_data( self._data[i] = new_array if old_data_shape != new_array.shape[-2:]: - # delete graphics at index zero - subplot.delete_graphic(graphic=subplot["image_widget_managed"]) - # insert new graphic at index zero + # make a new graphic with the new xy dims frame = self._process_indices( new_array, slice_indices=self._current_index ) frame = self._process_frame_apply(frame, i) + + # make new graphic first new_graphic = ImageGraphic(data=frame, name="image_widget_managed") - subplot.insert_graphic(graphic=new_graphic) + + # set hlut tool to use new graphic subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic + # delete old graphic after setting hlut tool to new graphic + # this ensures gc + subplot.delete_graphic(graphic=subplot["image_widget_managed"]) + subplot.insert_graphic(graphic=new_graphic) + if new_array.ndim > 2: # to set max of time slider, txy or tzxy max_lengths["t"] = min(max_lengths["t"], new_array.shape[0] - 1) From c7bab03380562e3784177db439b4f57ae5165ba8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 31 Mar 2024 22:50:37 -0400 Subject: [PATCH 8/8] black --- fastplotlib/graphics/selectors/_base_selector.py | 8 ++++++-- fastplotlib/layouts/_plot_area.py | 6 ++++-- fastplotlib/widgets/histogram_lut.py | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 93fa53081..f20eba4a0 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -138,7 +138,9 @@ def _fpl_add_plot_area_hook(self, plot_area): for fill in self._fill: if fill.material.color_is_transparent: self._pfunc_fill = partial(self._check_fill_pointer_event, fill) - self._plot_area.renderer.add_event_handler(self._pfunc_fill, "pointer_down") + self._plot_area.renderer.add_event_handler( + self._pfunc_fill, "pointer_down" + ) # when the pointer moves self._plot_area.renderer.add_event_handler(self._move, "pointer_move") @@ -359,6 +361,8 @@ def _key_up(self, ev): def _fpl_cleanup(self): if hasattr(self, "_pfunc_fill"): - self._plot_area.renderer.remove_event_handler(self._pfunc_fill, "pointer_down") + self._plot_area.renderer.remove_event_handler( + self._pfunc_fill, "pointer_down" + ) del self._pfunc_fill super()._fpl_cleanup() diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index eda0a3a08..37a25bbcc 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -71,12 +71,14 @@ def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: return tuple(proxies) - def get_refcounts(self) -> dict[HexStr: int]: + def get_refcounts(self) -> dict[HexStr:int]: counts = dict() for item in (self._graphics, self._selectors, self._legends): for k in item.keys(): - counts[(k, item[k].name, item[k].__class__.__name__)] = getrefcount(item[k]) + counts[(k, item[k].name, item[k].__class__.__name__)] = getrefcount( + item[k] + ) return counts diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 1e2fedb10..67af972b8 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -107,7 +107,9 @@ def __init__( self._text_vmax.position_x = -120 self._text_vmax.position_y = self._linear_region_selector.selection()[1] - self._linear_region_selector.selection.add_event_handler(self._linear_region_handler) + self._linear_region_selector.selection.add_event_handler( + self._linear_region_handler + ) self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy