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)