Repository: pyvista/pyvista

Issue:
Removing a mesh actor doesn't remove its scalar bar in 0.30.1
The recent update has led to some scalar bar issues when removing/adding mesh actors. If clearing the entire plotter after removing the mesh, the scalar bar disappears, but the lighting is also disrupted.
In PyVista 0.29.1, removing a mesh also removed it's scalar bar. I am using PyVista 0.30.1 and pyvistaqt 0.4.0 in these videos.

After doing a bunch of digging,  this is happening because `def _remove_mapper_from_plotter(...)` was removed from the renderers.py file and wasn't replaced anywhere in the new ScalarBars class. I am happy to try to help get this figured out, but I have no experience with Github management. 
 


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/1347?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#1347](https://codecov.io/gh/pyvista/pyvista/pull/1347?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (36b720a) into [master](https://codecov.io/gh/pyvista/pyvista/commit/a4f148d3d165648eb871ba45fa39f457377bd10e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (a4f148d) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #1347      +/-   ##
==========================================
+ Coverage   90.01%   90.03%   +0.01%     
==========================================
  Files          56       56              
  Lines       12188    12207      +19     
==========================================
+ Hits        10971    10990      +19     
  Misses       1217     1217              
```


Patch:
diff --git a/docs/extras/building_vtk.rst b/docs/extras/building_vtk.rst
index cce56b9f..63ef9883 100644
--- a/docs/extras/building_vtk.rst
+++ b/docs/extras/building_vtk.rst
@@ -31,14 +31,16 @@ resulting wheel size.
     #!/bin/bash
 
     # install build dependencies (Linux/Debian)
-    sudo apt-get install ninja-build cmake libgl1-mesa-dev python3-dev -y
+    sudo apt-get install ninja-build cmake libgl1-mesa-dev python3-dev
 
     # Linux/CentOS
-    sudo yum install epel-release -y
-    sudo yum install ninja-build cmake mesa-libGL-devel mesa-libGLU-devel -y
+    sudo yum install epel-release
+    sudo yum install ninja-build cmake mesa-libGL-devel mesa-libGLU-devel
 
     git clone https://github.com/Kitware/VTK
-    cd VTK
+    mkdir VTK/build
+    cd VTK/build
+    git checkout v9.0.1  # optional to select a version, but recommended
 
     PYBIN=/usr/bin/python3
     cmake -GNinja \
@@ -54,6 +56,9 @@ resulting wheel size.
           -DVTK_OPENGL_HAS_EGL=False \
           -DPython3_EXECUTABLE=$PYBIN ../
     ninja
+
+    # build wheel in dist
+    $PYBIN -m pip install wheel
     $PYBIN setup.py bdist_wheel
     pip install dist/vtk-*.whl  # optionally install it
 
@@ -62,18 +67,19 @@ resulting wheel size.
 
 Off-Screen Plotting GPU Support
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-VTK supports rendering with EGL, enabling rapid off-screen using GPU
-hardware acceleration.  The default VTK wheels are not built with this
-feature, but you can build VTK for off-screen plotting using GPU
-support by modifying the above ``cmake`` command with:
+VTK supports rendering with EGL, enabling rapid off-screen rendering
+using GPU hardware acceleration without installing a virtual
+framebuffer.  The default VTK wheels are not built with this feature,
+but you can build VTK for off-screen plotting using GPU support by
+modifying the above ``cmake`` command with:
 
 .. code-block:: bash
 
     # install OpenGL with EGL (Linux/Debian)
-    sudo apt-get install libegl1-mesa-dev -y
+    sudo apt-get install libegl1-mesa-dev
 
     # CentOS
-    sudo yum install mesa-libEGL-devel -y
+    sudo yum install mesa-libEGL-devel
 
     -DVTK_OPENGL_HAS_EGL=True \
 
@@ -83,7 +89,7 @@ static images or stream the render window to another computer with a
 display (e.g using ``pyvista.set_jupyter_backend('ipyvtklink')`` and
 jupyterlab). In other words, this wheel will make VTK unusable outside
 of an off-screen environment, so only plan on installing it on a
-headless system without a X server.
+headless system without an X server.
 
 
 Building ManyLinux Wheels
@@ -124,7 +130,7 @@ built on.  You can work around this by building your wheels using a
       ;;
     esac
 
-    yum install ninja-build cmake mesa-libGL-devel mesa-libGLU-devel -y
+    yum install ninja-build cmake mesa-libGL-devel mesa-libGLU-devel
 
     rm -rf /io/build
     mkdir /io/build -p
@@ -144,8 +150,9 @@ built on.  You can work around this by building your wheels using a
           -DPython3_EXECUTABLE=$PYBIN ../
     ninja-build
 
-    # build wheel
+    # build wheel in dist
     rm -rf dist
+    $PYBIN -m pip install wheel
     $PYBIN setup.py bdist_wheel
 
     # cleanup wheel
@@ -155,7 +162,7 @@ built on.  You can work around this by building your wheels using a
 
 This script can be called with:
 
-.. code::
+.. code-block:: bash
 
     PYTHON_VERSION=3.8
     docker run -e \
@@ -168,18 +175,20 @@ This script can be called with:
    dependencies.
 
 
-Python VTK Wheel on Raspberry PI (64-bit)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-While it's possible to build on 32-bit Raspberry Pi (ARMv7), there's
+Building Python VTK Wheel on Raspberry Pi (64-bit)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+While it's possible to build on 32-bit Raspberry Pi (ARMv7), there are
 several issues that crop up when building wheels for the 32-bit
 version (see `manylinux issue 84
-<https://github.com/pypa/manylinux/issues/84>`_.  Should you attempt
-to build on 32-bit , attempt to build the wheel using `dockcross
-<https://github.com/dockcross/dockcross>`_ as you may run into memory limitations otherwise (especially with only 1 GB RAM).
+<https://github.com/pypa/manylinux/issues/84>`_).  Should you attempt
+to build on 32-bit, try building the wheel using `dockcross
+<https://github.com/dockcross/dockcross>`_ as you may run into memory
+limitations otherwise (especially with only 1 GB RAM).
 
-Building the ``aarch64`` manylinux wheel can be done via docker with the ``quay.io/pypa/manylinux2014_aarch64`` image.  Run the following:
+Building the ``aarch64`` manylinux wheel can be done via docker with
+the ``quay.io/pypa/manylinux2014_aarch64`` image.  Run the following:
 
-.. code::
+.. code-block:: bash
 
     PYTHON_VERSION=3.6
     rm -rf build
@@ -189,8 +198,7 @@ Building the ``aarch64`` manylinux wheel can be done via docker with the ``quay.
 
 Where ``build_wheels.sh`` is:
 
-.. code::
-
+.. code-block:: bash
 
     #!/bin/bash
     # builds python wheels on docker container and tests installation
@@ -221,8 +229,9 @@ Where ``build_wheels.sh`` is:
     esac
 
     /bin/bash
-    yum install epel-release -y
-    yum install ninja-build mesa-libEGL-devel -y
+    yum install epel-release
+    yum install ninja-build
+    yum install mesa-libEGL-devel  # only needed when building EGL
 
     mkdir /io/build -p
     cd /io/build
@@ -237,7 +246,7 @@ Where ``build_wheels.sh`` is:
           -DVTK_WHEEL_BUILD=ON \
           -DVTK_PYTHON_VERSION=3 \
           -DVTK_WRAP_PYTHON=ON \
-          -DVTK_OPENGL_HAS_EGL=True \
+          -DVTK_OPENGL_HAS_EGL=False \
           -DPython3_EXECUTABLE=$PYBIN ../
     ninja-build
 
@@ -248,7 +257,7 @@ Where ``build_wheels.sh`` is:
     # cleanup wheel
     rm -rf wheelhouse
     auditwheel repair dist/*.whl
-    cp wheelhouse/vtk*.whl /io/wheels_egl
+    cp wheelhouse/vtk*.whl /io/wheels
 
 Be sure to either enable or disable ``DVTK_OPENGL_HAS_EGL`` depending
 on if you want ``EGL`` enabled for your wheel.
diff --git a/examples/01-filter/sampling_functions_2d.py b/examples/01-filter/sampling_functions_2d.py
new file mode 100644
index 00000000..3664c7cd
--- /dev/null
+++ b/examples/01-filter/sampling_functions_2d.py
@@ -0,0 +1,78 @@
+"""
+Sample Function: Perlin Noise in 2D
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Here we use :func:`pyvista.core.imaging.sample_function` to sample
+perlin noise over a region to generate random terrain.
+
+Perlin noise is atype of gradient noise often used by visual effects
+artists to increase the appearance of realism in computer graphics.
+Source:
+https://en.wikipedia.org/wiki/Perlin_noise
+
+The development of Perlin Noise has allowed computer graphics artists
+to better represent the complexity of natural phenomena in visual
+effects for the motion picture industry.
+
+"""
+
+import pyvista as pv
+
+###############################################################################
+# Generate Perlin Noise over a StructuredGrid
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Feel free to change the values of ``freq`` to change the shape of
+# the "mountains".  For example, lowering the frequency will make the
+# terrain seem more like hills rather than mountains.
+freq = [0.689, 0.562, 0.683]
+noise = pv.perlin_noise(1, freq, (0, 0, 0))
+sampled = pv.sample_function(noise,
+                             bounds=(-10, 10, -10, 10, -10, 10),
+                             dim=(500, 500, 1))
+
+
+###############################################################################
+# Warp by scalar
+# ~~~~~~~~~~~~~~
+# Here we warp by scalar to give the terrain some height based on the
+# value of the perlin noise.  This is necessary to the terrain its shape.
+
+mesh = sampled.warp_by_scalar('scalars')
+mesh = mesh.extract_surface()
+
+# clean and smooth a little to reduce perlin noise artifacts
+mesh = mesh.smooth(n_iter=100, inplace=False, relaxation_factor=1)
+
+# This makes the "water" level look flat.
+z = mesh.points[:, 2]
+diff = z.max() - z.min()
+
+# water level at 70%  (change this to change the water level)
+water_percent = 0.7
+water_level = z.max() - water_percent*diff
+mesh.points[z < water_level, 2] = water_level
+
+
+###############################################################################
+# Show the terrain as a contour plot
+
+# make the water blue
+rng = z.max() - z.min()
+clim = (z.max() - rng*1.65, z.max())
+
+pl = pv.Plotter()
+pl.add_mesh(mesh, scalars=z,
+            cmap='gist_earth', n_colors=10, show_scalar_bar=False,
+            smooth_shading=True, clim=clim)
+pl.show()
+
+
+###############################################################################
+# Show the terrain with custom lighting and shadows
+
+pl = pv.Plotter(lighting=None)
+pl.add_light(pv.Light((3, 1, 0.5), show_actor=True, positional=True,
+                      cone_angle=90, intensity=1.2))
+pl.add_mesh(mesh, cmap='gist_earth', show_scalar_bar=False,
+            smooth_shading=True, clim=clim)
+pl.enable_shadows = True
+pl.show()
diff --git a/examples/01-filter/sampling_functions_3d.py b/examples/01-filter/sampling_functions_3d.py
new file mode 100644
index 00000000..93097b4c
--- /dev/null
+++ b/examples/01-filter/sampling_functions_3d.py
@@ -0,0 +1,38 @@
+"""
+Sample Function: Perlin Noise in 3D
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Here we use :func:`pyvista.core.imaging.sample_function` to sample
+perlin noise over a region to generate random terrain.
+
+Video games like Minecraft use Perlin noise to create terrain.  Here,
+we create a voxelized mesh similar to a Minecraft "cave".
+
+"""
+
+import pyvista as pv
+
+###############################################################################
+# Generate Perlin Noise over a 3D StructuredGrid
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Feel free to change the values of ``freq`` to change the shape of
+# the "caves".  For example, lowering the frequency will make the
+# caves larger and more expansive, while a higher frequency in any
+# direction will make the caves appear more "vein-like" and less open.
+# 
+# Change the threshold to reduce or increase the percent of the
+# terrain that is open or closed
+
+freq = (1, 1, 1)
+noise = pv.perlin_noise(1, freq, (0, 0, 0))
+grid = pv.sample_function(noise, [0, 3.0, -0, 1.0, 0, 1.0], dim=(120, 40, 40))
+out = grid.threshold(0.02)
+out
+
+###############################################################################
+# color limits without blue
+mn, mx = [out['scalars'].min(), out['scalars'].max()]
+clim = (mn, mx*1.8)
+
+out.plot(cmap='gist_earth_r', background='white',
+         show_scalar_bar=False, lighting=True, clim=clim,
+         show_edges=False)
diff --git a/pyvista/_vtk.py b/pyvista/_vtk.py
index 95163d9f..dd809aee 100644
--- a/pyvista/_vtk.py
+++ b/pyvista/_vtk.py
@@ -20,6 +20,7 @@ except ImportError:  # pragma: no cover
 
 if VTK9:
 
+    from vtkmodules.vtkImagingHybrid import vtkSampleFunction
     from vtkmodules.vtkInteractionWidgets import (vtkScalarBarWidget,
                                                   vtkSplineWidget,
                                                   vtkSphereWidget,
@@ -106,7 +107,8 @@ if VTK9:
                                         vtkDataSetWriter,
                                         vtkPolyDataReader,
                                         vtkDataSetReader)
-    from vtkmodules.vtkCommonDataModel import (vtkDataObject,
+    from vtkmodules.vtkCommonDataModel import (vtkImplicitFunction,
+                                               vtkDataObject,
                                                vtkExplicitStructuredGrid,
                                                vtkPyramid,
                                                vtkPlane,
@@ -132,6 +134,7 @@ if VTK9:
                                                vtkStaticPointLocator,
                                                vtkSelectionNode,
                                                vtkSelection,
+                                               vtkPerlinNoise,
                                                VTK_HEXAHEDRON,
                                                VTK_PYRAMID,
                                                VTK_QUAD,
diff --git a/pyvista/core/__init__.py b/pyvista/core/__init__.py
index e32053f3..1b072e2b 100644
--- a/pyvista/core/__init__.py
+++ b/pyvista/core/__init__.py
@@ -9,3 +9,5 @@ from .grid import Grid, RectilinearGrid, UniformGrid
 from .objects import Table, Texture
 from .pointset import PointGrid, PolyData, StructuredGrid, UnstructuredGrid, ExplicitStructuredGrid
 from .pyvista_ndarray import pyvista_ndarray
+from .common_data import *
+from .imaging import *
diff --git a/pyvista/core/common_data.py b/pyvista/core/common_data.py
new file mode 100644
index 00000000..5af6d8c7
--- /dev/null
+++ b/pyvista/core/common_data.py
@@ -0,0 +1,59 @@
+"""Contains PyVista mappings from vtkmodules.vtkCommonDataModel."""
+from typing import Sequence
+
+from pyvista import _vtk
+
+
+def perlin_noise(amplitude, freq: Sequence[float], phase: Sequence[float]):
+    """Return the implicit function that implements Perlin noise.
+
+    Uses ``vtk.vtkPerlinNoise`` and computes a Perlin noise field as
+    an implicit function. ``vtk.vtkPerlinNoise`` is a concrete
+    implementation of ``vtk.vtkImplicitFunction``. Perlin noise,
+    originally described by Ken Perlin, is a non-periodic and
+    continuous noise function useful for modeling real-world objects.
+
+    The amplitude and frequency of the noise pattern are
+    adjustable. This implementation of Perlin noise is derived closely
+    from Greg Ward's version in Graphics Gems II.
+
+    Parameters
+    ----------
+    amplitude : float
+        Amplitude of the noise function.
+
+        ``amplitude`` can be negative. The noise function varies
+        randomly between ``-|Amplitude|`` and
+        ``|Amplitude|``. Therefore the range of values is
+        ``2*|Amplitude|`` large. The initial amplitude is 1.
+
+    freq : Sequence[float, float, float]
+        The frequency, or physical scale, of the noise function
+        (higher is finer scale).
+
+        The frequency can be adjusted per axis, or the same for all axes.
+
+    phase : Sequence[float, float, float]
+        Set/get the phase of the noise function.
+
+        This parameter can be used to shift the noise function within
+        space (perhaps to avoid a beat with a noise pattern at another
+        scale). Phase tends to repeat about every unit, so a phase of
+        0.5 is a half-cycle shift.
+
+    Examples
+    --------
+    Create a perlin noise function with an amplitude of 0.1, frequency
+    for all axes of 1, and a phase of 0 for all axes.
+
+    >>> import pyvista
+    >>> noise = perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
+
+    Apply the perlin noise function to 
+
+    """
+    noise = _vtk.vtkPerlinNoise()
+    noise.SetAmplitude(amplitude)
+    noise.SetFrequency(freq)
+    noise.SetPhase(phase)
+    return noise
diff --git a/pyvista/core/imaging.py b/pyvista/core/imaging.py
new file mode 100644
index 00000000..cf8186c3
--- /dev/null
+++ b/pyvista/core/imaging.py
@@ -0,0 +1,145 @@
+"""Contains PyVista mappings from vtkmodules.vtkImagingHybrid."""
+from typing import Sequence
+import sys, os
+
+import numpy as np
+
+from pyvista import _vtk, wrap
+from .filters import _get_output, _update_alg
+
+
+def sample_function(function: _vtk.vtkImplicitFunction,
+                    bounds: Sequence[float] = (-1.0, 1.0, -1.0, 1.0, -1.0, 1.0),
+                    dim: Sequence[int] = (50, 50, 50),
+                    compute_normals: bool = False,
+                    output_type: np.dtype = np.double,
+                    capping: bool = False,
+                    cap_value: float = sys.float_info.max,
+                    scalar_arr_name: str = "scalars",
+                    normal_arr_name: str = "normals",
+                    progress_bar: bool = False):
+    """Sample an implicit function over a structured point set.
+
+    Uses ``vtk.vtkSampleFunction``
+
+    This method evaluates an implicit function and normals at each
+    point in a ``vtk.vtkStructuredPoints``. The user can specify the
+    sample dimensions and location in space to perform the sampling.
+
+    To create closed surfaces (in conjunction with the
+    vtkContourFilter), capping can be turned on to set a particular
+    value on the boundaries of the sample space.
+
+    Parameters
+    ----------
+    function : vtk.vtkImplicitFunction
+        Implicit function to evaluate.  For example, the function
+        generated from ``perlin_noise``.
+
+    bounds : length 6 sequence
+        Specify the bounds in the format of:
+
+        - ``(xmin, xmax, ymin, ymax, zmin, zmax)``
+
+        Defaults to ``(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)``.
+
+    dim : length 3 sequence
+        Dimensions of the data on which to sample in the format of
+        ``(xdim, ydim, zdim)``.  Defaults to ``(50, 50, 50)``.
+
+    compute_normals : bool, optional
+        Enable or disable the computation of normals.  Default ``False``.
+
+    output_type : np.dtype, optional
+        Set the output scalar type.  Defaults to ``np.double``.  One
+        of the following:
+
+        - ``np.float64``
+        - ``np.float32``
+        - ``np.int64``
+        - ``np.uint64``
+        - ``np.int32``
+        - ``np.uint32``
+        - ``np.int16``
+        - ``np.uint16``
+        - ``np.int8``
+        - ``np.uint8``
+
+    capping : bool, optional
+        Enable or disable capping.  Default ``False``.  If capping is
+        enabled, then the outer boundaries of the structured point set
+        are set to cap value. This can be used to ensure surfaces are
+        closed.
+
+    cap_value : float, optional
+        Capping value used with the ``capping`` parameter.
+
+    scalar_arr_name : str, optional
+        Set the scalar array name for this data set.  Defaults to
+        ``"scalars"``
+
+    normal_arr_name : str, optional
+        Set the normal array name for this data set.  Defaults to
+        ``"normals"``
+
+    progress_bar : bool, optional
+        Display a progress bar to indicate progress.  Default
+        ``False``.
+
+    Examples
+    --------
+    Sample perlin noise over a structured grid in 3D.
+
+    >>> import pyvista
+    >>> noise = pyvista.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
+    >>> grid = pyvista.sample_function(noise, [0, 3.0, -0, 1.0, 0, 1.0],
+    ...                                dim=(60, 20, 20))
+    >>> out.plot(cmap='gist_earth_r', show_scalar_bar=False,
+    ...          show_edges=True)  # doctest:+SKIP
+
+    Sample perlin noise in 2D and plot it.
+
+    >>> noise = pyvista.perlin_noise(0.1, (5, 5, 5), (0, 0, 0))
+    >>> surf = pyvista.sample_function(noise, dim=(200, 200, 1))
+    >>> surf.plot()    # doctest:+SKIP
+
+    """
+    samp = _vtk.vtkSampleFunction()
+    samp.SetImplicitFunction(function)
+    samp.SetSampleDimensions(dim)
+    samp.SetModelBounds(bounds)
+    samp.SetComputeNormals(compute_normals)
+    samp.SetCapping(capping)
+    samp.SetCapValue(cap_value)
+    samp.SetNormalArrayName(normal_arr_name)
+    samp.SetScalarArrayName(scalar_arr_name)
+
+    if output_type == np.float64:
+        samp.SetOutputScalarTypeToDouble()
+    elif output_type == np.float32:
+        samp.SetOutputScalarTypeToFloat()
+    elif output_type == np.int64:
+        if os.name == 'nt':
+            raise ValueError('This function on Windows only supports int32 or smaller')
+        samp.SetOutputScalarTypeToLong()
+    elif output_type == np.uint64:
+        if os.name == 'nt':
+            raise ValueError('This function on Windows only supports int32 or smaller')
+        samp.SetOutputScalarTypeToUnsignedLong()
+    elif output_type == np.int32:
+        samp.SetOutputScalarTypeToInt()
+    elif output_type == np.uint32:
+        samp.SetOutputScalarTypeToUnsignedInt()
+    elif output_type == np.int16:
+        samp.SetOutputScalarTypeToShort()
+    elif output_type == np.uint16:
+        samp.SetOutputScalarTypeToUnsignedShort()
+    elif output_type == np.int8:
+        samp.SetOutputScalarTypeToChar()
+    elif output_type == np.uint8:
+        samp.SetOutputScalarTypeToUnsignedChar()
+    else:
+        raise ValueError(f'Invalid output_type {output_type}')
+
+    _update_alg(samp, progress_bar=progress_bar, message='Sampling')
+    return wrap(samp.GetOutput())
diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py
index 1bc25daa..d6e7f226 100644
--- a/pyvista/plotting/renderer.py
+++ b/pyvista/plotting/renderer.py
@@ -1280,6 +1280,8 @@ class Renderer(_vtk.vtkRenderer):
         if actor is None:
             return False
 
+        # ensure any scalar bars associated with this actor are removed
+        self.parent.scalar_bars._remove_mapper_from_plotter(actor)
         self.RemoveActor(actor)
 
         if name is None:
diff --git a/pyvista/plotting/scalar_bars.py b/pyvista/plotting/scalar_bars.py
index 6f033f53..d4d2f1ea 100644
--- a/pyvista/plotting/scalar_bars.py
+++ b/pyvista/plotting/scalar_bars.py
@@ -28,12 +28,34 @@ class ScalarBars():
         """Nice representation of this class."""
         lines = []
         lines.append('Scalar Bar Title     Interactive')
-        for title, actor in self._scalar_bar_actors.items():
+        for title in self._scalar_bar_actors:
             interactive = title in self._scalar_bar_widgets
             title = f'"{title}"'
             lines.append(f'{title:20} {str(interactive):5}')
         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."""
+        try:
+            mapper = actor.GetMapper()
+        except AttributeError:
+            return
+        for name in list(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:
+                    self._scalar_bar_mappers.pop(name)
+                    self._scalar_bar_ranges.pop(name)
+                    self._plotter.remove_actor(self._scalar_bar_actors.pop(name),
+                                               reset_camera=reset_camera,
+                                               render=render)
+                    self._plotter._scalar_bar_slots.add(slot)
+            return
+
     def remove_scalar_bar(self, title=None, render=True):
         """Remove a scalar bar.
 
diff --git a/requirements_style.txt b/requirements_style.txt
index 06fb0b5d..b3130376 100644
--- a/requirements_style.txt
+++ b/requirements_style.txt
@@ -1,2 +1,2 @@
-codespell==1.17.1
+codespell==2.0.0
 pydocstyle==5.1.1


Test patch:
diff --git a/tests/test_imaging.py b/tests/test_imaging.py
new file mode 100644
index 00000000..34efe2b6
--- /dev/null
+++ b/tests/test_imaging.py
@@ -0,0 +1,53 @@
+import os
+
+import pytest
+import numpy as np
+from numpy.testing import assert_allclose
+
+import pyvista as pv
+
+
+def test_perlin_noise():
+    amp = 10
+    freq = (3, 2, 1)
+    phase = (0, 0, 0)
+    perlin = pv.perlin_noise(amp, freq, phase)
+
+    assert_allclose(perlin.GetAmplitude(), amp)
+    assert_allclose(perlin.GetFrequency(), freq)
+    assert_allclose(perlin.GetPhase(), phase)
+
+
+
+@pytest.mark.parametrize('dtype', [
+    np.float64,
+    np.float32,
+    np.int64,
+    np.uint64,
+    np.int32,
+    np.uint32,
+    np.int16,
+    np.uint16,
+    np.int8,
+    np.uint8,
+])
+def test_sample_function(dtype):
+    perlin = pv.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
+    bounds = (0, 2, 0, 1, -4, 4)
+    dim = (5, 10, 20)
+    scalar_arr_name = 'my_scalars'
+
+    if os.name == 'nt' and dtype in [np.int64, np.uint64]:
+        with pytest.raises(ValueError):
+            pv.sample_function(perlin, output_type=dtype)
+    else:
+        mesh = pv.sample_function(perlin,
+                                  bounds,
+                                  dim,
+                                  compute_normals=False,
+                                  output_type=dtype,
+                                  scalar_arr_name=scalar_arr_name)
+    
+        assert_allclose(mesh.dimensions, dim)
+        assert_allclose(mesh.bounds, bounds)
+        assert mesh[scalar_arr_name].dtype == dtype
diff --git a/tests/test_scalar_bars.py b/tests/test_scalar_bars.py
index fe3cca21..e474d60b 100644
--- a/tests/test_scalar_bars.py
+++ b/tests/test_scalar_bars.py
@@ -54,3 +54,16 @@ def test_clear(scalar_bars):
     assert len(scalar_bars) == 1
     scalar_bars.clear()
     assert len(scalar_bars) == 0
+
+
+def test_actor_removal(sphere):
+    # verify that when removing an actor we also remove the
+    # corresponding scalar bar
+
+    sphere['scalars'] = sphere.points[:, 2]
+
+    pl = pv.Plotter()
+    actor = pl.add_mesh(sphere, show_scalar_bar=True)
+    assert len(pl.scalar_bars) == 1
+    pl.remove_actor(actor)
+    assert len(pl.scalar_bars) == 0