Repository: pyvista/pyvista

Issue:
remove_scalar_bar doesn't release the slot in _scalar_bar_slots and the slot limit can be reached
When using the method `remove_scalar_bar`, the code goes there:
https://github.com/pyvista/pyvista/blob/a3711401a7bd37f655fc346a558f7c67cb522eab/pyvista/plotting/scalar_bars.py#L37-L57

but `self._scalar_bar_mappers` is already empty so it never goes into the for loop and cannot reach the line `self._plotter._scalar_bar_slots.add(slot)` where it should release the slot.

the set of available slots follows this pattern: `{4, 5, 6, 7, 8, 9}` with always only one scalar_bar available and at some point it reaches the limit.

What is the expected behavior? What if there is one or many meshes using that scalar_bar when using that method?

P.S. I just tried overwriting multiple scale_bars with multiple meshes, trying to break it and it works as intented as long as I don't use `remove_scalar_bar` explicitely.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/1553?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#1553](https://codecov.io/gh/pyvista/pyvista/pull/1553?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (8df1844) into [main](https://codecov.io/gh/pyvista/pyvista/commit/1e82020eff67c0ce2530747421b1d71ec747973e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (1e82020) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #1553   +/-   ##
=======================================
  Coverage   90.17%   90.17%           
=======================================
  Files          68       68           
  Lines       12910    12913    +3     
=======================================
+ Hits        11641    11644    +3     
  Misses       1269     1269           
```


Patch:
diff --git a/pyvista/plotting/scalar_bars.py b/pyvista/plotting/scalar_bars.py
index d9cf9c95..e40e3f9d 100644
--- a/pyvista/plotting/scalar_bars.py
+++ b/pyvista/plotting/scalar_bars.py
@@ -35,16 +35,23 @@ class ScalarBars():
         return '\n'.join(lines)
 
     def _remove_mapper_from_plotter(self, actor, reset_camera=False, render=False):
-        """Remove an actor's mapper from the given plotter's _scalar_bar_mappers."""
+        """Remove an actor's mapper from the given plotter's _scalar_bar_mappers.
+
+        This ensures that when actors are removed, their corresponding
+        scalar bars are removed.
+
+        """
         try:
             mapper = actor.GetMapper()
         except AttributeError:
             return
-        for name in list(self._scalar_bar_mappers):
+
+        for name in self._scalar_bar_mappers:
             try:
                 self._scalar_bar_mappers[name].remove(mapper)
             except ValueError:
                 pass
+
             if not self._scalar_bar_mappers[name]:
                 slot = self._plotter._scalar_bar_slot_lookup.pop(name, None)
                 if slot is not None:
@@ -79,15 +86,20 @@ class ScalarBars():
             else:
                 title = list(self._scalar_bar_actors.keys())[0]
 
+        actor = self._scalar_bar_actors.pop(title)
+        self._plotter.remove_actor(actor, render=render)
         self._scalar_bar_ranges.pop(title)
         self._scalar_bar_mappers.pop(title)
+
+        # add back in the scalar bar slot
+        slot = self._plotter._scalar_bar_slot_lookup.pop(title, None)
+        if slot is not None:
+            self._plotter._scalar_bar_slots.add(slot)
+
         widget = self._scalar_bar_widgets.pop(title, None)
         if widget is not None:
             widget.SetEnabled(0)
 
-        actor = self._scalar_bar_actors.pop(title)
-        self._plotter.remove_actor(actor, render=render)
-
     def __len__(self):
         """Return the number of scalar bar actors."""
         return len(self._scalar_bar_actors)
@@ -436,7 +448,6 @@ class ScalarBars():
         title_text.SetColor(color)
 
         self._scalar_bar_actors[title] = scalar_bar
-
         if interactive:
             scalar_widget = _vtk.vtkScalarBarWidget()
             scalar_widget.SetScalarBarActor(scalar_bar)
@@ -468,4 +479,5 @@ class ScalarBars():
         # finally, add to the actor and return the scalar bar
         self._plotter.add_actor(scalar_bar, reset_camera=False,
                                 pickable=False, render=render)
+
         return scalar_bar


Test patch:
diff --git a/tests/test_scalar_bars.py b/tests/test_scalar_bars.py
index e474d60b..97d1ca6b 100644
--- a/tests/test_scalar_bars.py
+++ b/tests/test_scalar_bars.py
@@ -67,3 +67,14 @@ def test_actor_removal(sphere):
     assert len(pl.scalar_bars) == 1
     pl.remove_actor(actor)
     assert len(pl.scalar_bars) == 0
+
+
+def test_add_remove_bar(sphere):
+    pl = pv.Plotter()
+    pl.add_mesh(sphere, scalars=sphere.points[:, 2], show_scalar_bar=False)
+
+    # verify that the number of slots is restored
+    init_slots = len(pl._scalar_bar_slots)
+    pl.add_scalar_bar()
+    pl.remove_scalar_bar()
+    assert len(pl._scalar_bar_slots) == init_slots