Repository: pyvista/pyvista

Issue:
Unexpected threshold behavior
### Describe the bug, what's wrong, and what you expected.

I'm using simple structed grids of cells, and need to filter-out some "nodata" cells. To do this, I'm setting scalar values to the cell data, then using [threshold](https://docs.pyvista.org/api/core/_autosummary/pyvista.DataSetFilters.threshold.html) with the nodata value with `invert=True`. However, I'm getting confusing and inconsistent results compared to ParaView.

### Steps to reproduce the bug.

```python
import numpy as np
import pyvista

x = np.arange(5, dtype=float)
y = np.arange(6, dtype=float)
z = np.arange(2, dtype=float)
xx, yy, zz = np.meshgrid(x, y, z)
mesh = pyvista.StructuredGrid(xx, yy, zz)
mesh.cell_data.set_scalars(np.repeat(range(5), 4))

# All data
mesh.plot(show_edges=True)
# output is normal

# Filtering out nodata (zero) values
mesh.threshold(0, invert=True).plot(show_edges=True)
# output does not look normal, only 0-value cells are shown
```

### System Information

```shell
--------------------------------------------------------------------------------
  Date: Thu Nov 17 15:23:57 2022 New Zealand Daylight Time

                OS : Windows
            CPU(s) : 12
           Machine : AMD64
      Architecture : 64bit
               RAM : 31.7 GiB
       Environment : IPython
        GPU Vendor : NVIDIA Corporation
      GPU Renderer : NVIDIA RTX A4000/PCIe/SSE2
       GPU Version : 4.5.0 NVIDIA 472.39

  Python 3.9.13 | packaged by conda-forge | (main, May 27 2022, 16:50:36) [MSC
  v.1929 64 bit (AMD64)]

           pyvista : 0.37.0
               vtk : 9.1.0
             numpy : 1.22.3
           imageio : 2.22.0
            scooby : 0.7.0
             pooch : v1.6.0
        matplotlib : 3.6.2
             PyQt5 : 5.12.3
           IPython : 8.6.0
          colorcet : 3.0.1
             scipy : 1.8.0
              tqdm : 4.63.0
            meshio : 5.3.4
--------------------------------------------------------------------------------
```


### Screenshots

Normal looking whole grid:
![image](https://user-images.githubusercontent.com/895458/202339692-5046b23f-c3c8-4b2c-aaa7-4aa06afbae9f.png)

Odd-looking threshold attempt with pyvista, showing only 0-values:
![image](https://user-images.githubusercontent.com/895458/202339879-b2270e4c-a71b-4d43-86f8-4f67445b7b69.png)

Expected result with ParaView theshold filter with upper/lower set to 0 and invert selected:
![image](https://user-images.githubusercontent.com/895458/202340379-fea26838-b0f4-4828-b510-825f53522e87.png)

Apologies for any "user error", as I'm new to this package.


Discussion:
Can confirm, and I am seeing quite a few inconsistent results with the threshold filter. So this is not a user error!

 For example `inverter=True/False` should produce two logical inverses for this mesh, but it doesn't:
<img width="624" alt="Screen Shot 2022-11-16 at 11 18 48 PM" src="https://user-images.githubusercontent.com/22067021/202371190-7dcd64df-1882-4876-b4c3-43fe614913a6.png">


Second, depending on if `value` is a single value `0` or a range `[0, 0]` yields completely different results from the PyVista filter (but not in ParaView):


```py
p = pv.Plotter(notebook=0, shape=(1,2))
p.add_mesh(mesh.threshold(0, invert=False))
p.subplot(0,1)
p.add_mesh(mesh.threshold(0, invert=True))
p.link_views()
p.view_isometric()
p.show()
```

<img width="624" alt="Screen Shot 2022-11-16 at 11 20 01 PM" src="https://user-images.githubusercontent.com/22067021/202371391-393280c1-1091-4c61-82b2-e95e76b49327.png">


vs.

```py
p = pv.Plotter(notebook=0, shape=(1,2))
p.add_mesh(mesh.threshold([0, 0], invert=False))
p.subplot(0,1)
p.add_mesh(mesh.threshold([0, 0], invert=True))
p.link_views()
p.view_isometric()
p.show()
```

<img width="624" alt="Screen Shot 2022-11-16 at 11 20 34 PM" src="https://user-images.githubusercontent.com/22067021/202371476-cfd0fabb-daad-47db-acc5-855b43504f21.png">


This is not good... I'll start digging into this and see if I can fix the `threshold` filter such that I has consistency with itself and with ParaView

Thanks for taking a closer look. I should have mentioned that I get the same behavior on linux using a similar conda-forge setup.

I've found that ranges like `[1, 1]` work as expected to filter on values == 1, but to filter on zero, it needs to span a very small range:
```python
p = pv.Plotter(notebook=0, shape=(1,2))
p.add_mesh(mesh.threshold([-1e-30, 1e-30], invert=False))
p.subplot(0,1)
p.add_mesh(mesh.threshold([-1e-30, 1e-30], invert=True))
p.link_views()
p.view_isometric()
p.show()
```
which also logs this message three times:
> 2022-11-17 22:30:30.021 ( 559.089s) [        20F0C740]       vtkThreshold.cxx:96    WARN| vtkThreshold::ThresholdBetween was deprecated for VTK 9.1 and will be removed in a future version.

Another solution is to use [`extract_cells`](https://docs.pyvista.org/api/core/_autosummary/pyvista.StructuredGrid.extract_cells.html):
```python
mesh.extract_cells(mesh.cell_data.active_scalars != 0).plot()
mesh.extract_cells(mesh.cell_data.active_scalars != 1).plot()
```

Patch:
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -937,9 +937,10 @@ def threshold(
         continuous=False,
         preference='cell',
         all_scalars=False,
-        progress_bar=False,
-        component_mode="all",
+        component_mode='all',
         component=0,
+        method='upper',
+        progress_bar=False,
     ):
         """Apply a ``vtkThreshold`` filter to the input dataset.
 
@@ -956,43 +957,54 @@ def threshold(
            thresholding depends on whether that point is part of a cell that
            is kept after thresholding.
 
+           Please also note the default ``preference`` choice for CELL data
+           over POINT data. This is contrary to most other places in PyVista's
+           API where the preference typically defaults to POINT data. We chose
+           to prefer CELL data here so that if thresholding by a named array
+           that exists for both the POINT and CELL data, this filter will
+           default to the CELL data array while performing the CELL-wise
+           operation.
+
         Parameters
         ----------
         value : float or sequence, optional
-            Single value or (min, max) to be used for the data threshold.  If
+            Single value or (min, max) to be used for the data threshold. If
             a sequence, then length must be 2. If no value is specified, the
             non-NaN data range will be used to remove any NaN values.
+            Please reference the ``method`` parameter for how single values
+            are handled.
 
         scalars : str, optional
             Name of scalars to threshold on. Defaults to currently active scalars.
 
-        invert : bool, optional
-            If value is a single value, when invert is ``True`` cells
-            are kept when their values are below parameter ``"value"``.
-            When invert is ``False`` cells are kept when their value is
-            above the threshold ``"value"``.  Default is ``False``:
-            yielding above the threshold ``"value"``.
+        invert : bool, default: False
+            Invert the threshold results. That is, cells that would have been
+            in the output with this option off are excluded, while cells that
+            would have been excluded from the output are included.
 
-        continuous : bool, optional
+            .. warning::
+                This option is only supported for VTK version 9+
+
+        continuous : bool, default: False
             When True, the continuous interval [minimum cell scalar,
             maximum cell scalar] will be used to intersect the threshold bound,
             rather than the set of discrete scalar values from the vertices.
 
-        preference : str, optional
+        preference : str, default: 'cell'
             When ``scalars`` is specified, this is the preferred array
             type to search for in the dataset.  Must be either
-            ``'point'`` or ``'cell'``.
+            ``'point'`` or ``'cell'``. Throughout PyVista, the preference
+            is typically ``'point'`` but since the threshold filter is a
+            cell-wise operation, we prefer cell data for thresholding
+            operations.
 
-        all_scalars : bool, optional
+        all_scalars : bool, default: False
             If using scalars from point data, all
             points in a cell must satisfy the threshold when this
             value is ``True``.  When ``False``, any point of the cell
             with a scalar value satisfying the threshold criterion
             will extract the cell. Has no effect when using cell data.
 
-        progress_bar : bool, optional
-            Display a progress bar to indicate progress.
-
         component_mode : {'selected', 'all', 'any'}
             The method to satisfy the criteria for the threshold of
             multicomponent scalars.  'selected' (default)
@@ -1004,6 +1016,17 @@ def threshold(
             When using ``component_mode='selected'``, this sets
             which component to threshold on.
 
+        method : str, default: 'upper'
+            Set the threshold method for single-values, defining which
+            threshold bounds to use. If the ``value`` is a range, this
+            parameter will be ignored, extracting data between the two
+            values. For single values, ``'lower'`` will extract data
+            lower than the  ``value``. ``'upper'`` will extract data
+            larger than the ``value``.
+
+        progress_bar : bool, default: False
+            Display a progress bar to indicate progress.
+
         Returns
         -------
         pyvista.UnstructuredGrid
@@ -1057,31 +1080,6 @@ def threshold(
 
         field = get_array_association(self, scalars, preference=preference)
 
-        # If using an inverted range, merge the result of two filters:
-        if isinstance(value, (np.ndarray, collections.abc.Sequence)) and invert:
-            valid_range = [np.nanmin(arr), np.nanmax(arr)]
-            # Create two thresholds
-            t1 = self.threshold(
-                [valid_range[0], value[0]],
-                scalars=scalars,
-                continuous=continuous,
-                preference=preference,
-                invert=False,
-            )
-            t2 = self.threshold(
-                [value[1], valid_range[1]],
-                scalars=scalars,
-                continuous=continuous,
-                preference=preference,
-                invert=False,
-            )
-            # Use an AppendFilter to merge the two results
-            appender = _vtk.vtkAppendFilter()
-            appender.AddInputData(t1)
-            appender.AddInputData(t2)
-            _update_alg(appender, progress_bar, 'Thresholding')
-            return _get_output(appender)
-
         # Run a standard threshold algorithm
         alg = _vtk.vtkThreshold()
         alg.SetAllScalars(all_scalars)
@@ -1094,18 +1092,9 @@ def threshold(
         # use valid range if no value given
         if value is None:
             value = self.get_data_range(scalars)
-        # check if value is a sequence (if so threshold by min max range like ParaView)
-        if isinstance(value, (np.ndarray, collections.abc.Sequence)):
-            if len(value) != 2:
-                raise ValueError(
-                    f'Value range must be length one for a float value or two for min/max; not ({value}).'
-                )
-            alg.ThresholdBetween(value[0], value[1])
-        elif isinstance(value, collections.abc.Iterable):
-            raise TypeError('Value must either be a single scalar or a sequence.')
-        else:
-            # just a single value
-            _set_threshold_limit(alg, value, invert)
+
+        _set_threshold_limit(alg, value, method, invert)
+
         if component_mode == "component":
             alg.SetComponentModeToUseSelected()
             dim = arr.shape[1]
@@ -1124,6 +1113,7 @@ def threshold(
             raise ValueError(
                 f"component_mode must be 'component', 'all', or 'any' got: {component_mode}"
             )
+
         # Run the threshold
         _update_alg(alg, progress_bar, 'Thresholding')
         return _get_output(alg)
@@ -1135,6 +1125,7 @@ def threshold_percent(
         invert=False,
         continuous=False,
         preference='cell',
+        method='upper',
         progress_bar=False,
     ):
         """Threshold the dataset by a percentage of its range on the active scalars array.
@@ -1155,25 +1146,36 @@ def threshold_percent(
         scalars : str, optional
             Name of scalars to threshold on. Defaults to currently active scalars.
 
-        invert : bool, optional
-            When invert is ``True`` cells are kept when their values are
-            below the percentage of the range.  When invert is
-            ``False``, cells are kept when their value is above the
-            percentage of the range. Default is ``False``: yielding
-            above the threshold ``"value"``.
-
-        continuous : bool, optional
-            When ``True``, the continuous interval [minimum cell scalar,
-            maximum cell scalar] will be used to intersect the threshold
-            bound, rather than the set of discrete scalar values from
-            the vertices.
+        invert : bool, default: False
+            Invert the threshold results. That is, cells that would have been
+            in the output with this option off are excluded, while cells that
+            would have been excluded from the output are included.
 
-        preference : str, optional
+            .. warning::
+                This option is only supported for VTK version 9+
+
+        continuous : bool, default: False
+            When True, the continuous interval [minimum cell scalar,
+            maximum cell scalar] will be used to intersect the threshold bound,
+            rather than the set of discrete scalar values from the vertices.
+
+        preference : str, default: 'cell'
             When ``scalars`` is specified, this is the preferred array
             type to search for in the dataset.  Must be either
-            ``'point'`` or ``'cell'``.
+            ``'point'`` or ``'cell'``. Throughout PyVista, the preference
+            is typically ``'point'`` but since the threshold filter is a
+            cell-wise operation, we prefer cell data for thresholding
+            operations.
+
+        method : str, default: 'upper'
+            Set the threshold method for single-values, defining which
+            threshold bounds to use. If the ``value`` is a range, this
+            parameter will be ignored, extracting data between the two
+            values. For single values, ``'lower'`` will extract data
+            lower than the  ``value``. ``'upper'`` will extract data
+            larger than the ``value``.
 
-        progress_bar : bool, optional
+        progress_bar : bool, default: False
             Display a progress bar to indicate progress.
 
         Returns
@@ -1239,6 +1241,7 @@ def _get_val(percent, dmin, dmax):
             invert=invert,
             continuous=continuous,
             preference=preference,
+            method=method,
             progress_bar=progress_bar,
         )
 
@@ -5482,20 +5485,58 @@ def separate_cells(self):
         return self.shrink(1.0)
 
 
-def _set_threshold_limit(alg, value, invert):
-    """Set vtkThreshold limit.
+def _set_threshold_limit(alg, value, method, invert):
+    """Set vtkThreshold limits and function.
+
+    Addresses VTK API deprecations and previous PyVista inconsistencies with ParaView. Reference:
 
-    Addresses VTK API deprecation as pointed out in
-    https://github.com/pyvista/pyvista/issues/2850
+    * https://github.com/pyvista/pyvista/issues/2850
+    * https://github.com/pyvista/pyvista/issues/3610
+    * https://discourse.vtk.org/t/unnecessary-vtk-api-change/9929
 
     """
-    if invert:
-        if pyvista.vtk_version_info >= (9, 1):
-            alg.SetUpperThreshold(value)
-        else:  # pragma: no cover
-            alg.ThresholdByLower(value)
-    else:
-        if pyvista.vtk_version_info >= (9, 1):
-            alg.SetLowerThreshold(value)
-        else:  # pragma: no cover
-            alg.ThresholdByUpper(value)
+    # Check value
+    if isinstance(value, (np.ndarray, collections.abc.Sequence)):
+        if len(value) != 2:
+            raise ValueError(
+                f'Value range must be length one for a float value or two for min/max; not ({value}).'
+            )
+        # Check range
+        if value[0] > value[1]:
+            raise ValueError(
+                'Value sequence is invalid, please use (min, max). The provided first value is greater than the second.'
+            )
+    elif isinstance(value, collections.abc.Iterable):
+        raise TypeError('Value must either be a single scalar or a sequence.')
+    if pyvista.vtk_version_info >= (9,):
+        alg.SetInvert(invert)
+    elif invert:  # pragma: no cover
+        raise ValueError('PyVista no longer supports inverted thresholds for VTK<9.')
+    # Set values and function
+    if pyvista.vtk_version_info >= (9, 1):
+        if isinstance(value, (np.ndarray, collections.abc.Sequence)):
+            alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_BETWEEN)
+            alg.SetLowerThreshold(value[0])
+            alg.SetUpperThreshold(value[1])
+        else:
+            # Single value
+            if method.lower() == 'lower':
+                alg.SetLowerThreshold(value)
+                alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_LOWER)
+            elif method.lower() == 'upper':
+                alg.SetUpperThreshold(value)
+                alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_UPPER)
+            else:
+                raise ValueError('Invalid method choice. Either `lower` or `upper`')
+    else:  # pragma: no cover
+        # ThresholdByLower, ThresholdByUpper, ThresholdBetween
+        if isinstance(value, (np.ndarray, collections.abc.Sequence)):
+            alg.ThresholdBetween(value[0], value[1])
+        else:
+            # Single value
+            if method.lower() == 'lower':
+                alg.ThresholdByLower(value)
+            elif method.lower() == 'upper':
+                alg.ThresholdByUpper(value)
+            else:
+                raise ValueError('Invalid method choice. Either `lower` or `upper`')
diff --git a/pyvista/plotting/widgets.py b/pyvista/plotting/widgets.py
--- a/pyvista/plotting/widgets.py
+++ b/pyvista/plotting/widgets.py
@@ -1247,6 +1247,8 @@ def add_mesh_threshold(
         pointa=(0.4, 0.9),
         pointb=(0.9, 0.9),
         continuous=False,
+        all_scalars=False,
+        method='upper',
         **kwargs,
     ):
         """Apply a threshold on a mesh with a slider.
@@ -1265,8 +1267,13 @@ def add_mesh_threshold(
         scalars : str, optional
             The string name of the scalars on the mesh to threshold and display.
 
-        invert : bool, optional
-            Invert (flip) the threshold.
+        invert : bool, default: False
+            Invert the threshold results. That is, cells that would have been
+            in the output with this option off are excluded, while cells that
+            would have been excluded from the output are included.
+
+            .. warning::
+                This option is only supported for VTK version 9+
 
         widget_color : color_like, optional
             Color of the widget.  Either a string, RGB sequence, or
@@ -1277,7 +1284,7 @@ def add_mesh_threshold(
             * ``color=[1.0, 1.0, 1.0]``
             * ``color='#FFFFFF'``
 
-        preference : str, optional
+        preference : str, default: 'cell'
             When ``mesh.n_points == mesh.n_cells`` and setting
             scalars, this parameter sets how the scalars will be
             mapped to the mesh.  Default ``'cell'``, causes the
@@ -1287,20 +1294,35 @@ def add_mesh_threshold(
         title : str, optional
             The string label of the slider widget.
 
-        pointa : sequence, optional
+        pointa : sequence, default: (0.4, 0.9)
             The relative coordinates of the left point of the slider
             on the display port.
 
-        pointb : sequence, optional
+        pointb : sequence, default: (0.9, 0.9)
             The relative coordinates of the right point of the slider
             on the display port.
 
-        continuous : bool, optional
+        continuous : bool, default: False
             If this is enabled (default is ``False``), use the continuous
             interval ``[minimum cell scalar, maximum cell scalar]``
             to intersect the threshold bound, rather than the set of
             discrete scalar values from the vertices.
 
+        all_scalars : bool, default: False
+            If using scalars from point data, all
+            points in a cell must satisfy the threshold when this
+            value is ``True``.  When ``False``, any point of the cell
+            with a scalar value satisfying the threshold criterion
+            will extract the cell. Has no effect when using cell data.
+
+        method : str, default: 'upper'
+            Set the threshold method for single-values, defining which
+            threshold bounds to use. If the ``value`` is a range, this
+            parameter will be ignored, extracting data between the two
+            values. For single values, ``'lower'`` will extract data
+            lower than the  ``value``. ``'upper'`` will extract data
+            larger than the ``value``.
+
         **kwargs : dict, optional
             All additional keyword arguments are passed to ``add_mesh`` to
             control how the mesh is displayed.
@@ -1338,12 +1360,13 @@ def add_mesh_threshold(
             0, 0, 0, field.value, scalars
         )  # args: (idx, port, connection, field, name)
         alg.SetUseContinuousCellRange(continuous)
+        alg.SetAllScalars(all_scalars)
 
         threshold_mesh = pyvista.wrap(alg.GetOutput())
         self.threshold_meshes.append(threshold_mesh)
 
         def callback(value):
-            _set_threshold_limit(alg, value, invert)
+            _set_threshold_limit(alg, value, method, invert)
             alg.Update()
             threshold_mesh.shallow_copy(alg.GetOutput())
 


Test patch:
diff --git a/tests/plotting/test_widgets.py b/tests/plotting/test_widgets.py
--- a/tests/plotting/test_widgets.py
+++ b/tests/plotting/test_widgets.py
@@ -150,10 +150,12 @@ def test_widget_slider(uniform):
     p.add_slider_widget(callback=func, rng=[0, 10], style="modern", pass_widget=True)
     p.close()
 
-    p = pyvista.Plotter()
-    p.add_mesh_threshold(uniform, invert=True)
-    p.add_mesh(uniform.outline())
-    p.close()
+    if pyvista.vtk_version_info >= (9,):
+        # Invert not support for VTK8.1.2
+        p = pyvista.Plotter()
+        p.add_mesh_threshold(uniform, invert=True)
+        p.add_mesh(uniform.outline())
+        p.close()
 
     p = pyvista.Plotter()
     p.add_mesh_threshold(uniform, invert=False)
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -348,9 +348,10 @@ def test_threshold(datasets):
     thresh = dataset.threshold([100, 500], invert=False, progress_bar=True)
     assert thresh is not None
     assert isinstance(thresh, pyvista.UnstructuredGrid)
-    thresh = dataset.threshold([100, 500], invert=True, progress_bar=True)
-    assert thresh is not None
-    assert isinstance(thresh, pyvista.UnstructuredGrid)
+    if pyvista.vtk_version_info >= (9,):
+        thresh = dataset.threshold([100, 500], invert=True, progress_bar=True)
+        assert thresh is not None
+        assert isinstance(thresh, pyvista.UnstructuredGrid)
     # allow Sequence but not Iterable
     with pytest.raises(TypeError):
         dataset.threshold({100, 500}, progress_bar=True)
@@ -364,6 +365,16 @@ def test_threshold(datasets):
     with pytest.raises(ValueError):
         dataset.threshold([10, 100, 300], progress_bar=True)
 
+    if pyvista.vtk_version_info < (9,):
+        with pytest.raises(ValueError):
+            dataset.threshold([100, 500], invert=True)
+
+    with pytest.raises(ValueError):
+        dataset.threshold(100, method='between')
+
+    with pytest.raises(ValueError):
+        dataset.threshold((2, 1))
+
 
 def test_threshold_all_scalars():
     mesh = pyvista.Sphere()
@@ -434,6 +445,8 @@ def test_threshold_percent(datasets):
     inverts = [False, True, False, True, False]
     # Only test data sets that have arrays
     for i, dataset in enumerate(datasets[0:3]):
+        if inverts[i] and pyvista.vtk_version_info < (9,):
+            continue
         thresh = dataset.threshold_percent(
             percent=percents[i], invert=inverts[i], progress_bar=True
         )
@@ -450,6 +463,106 @@ def test_threshold_percent(datasets):
         dataset.threshold_percent({18.0, 85.0})
 
 
+@pytest.mark.skipif(
+    pyvista.vtk_version_info < (9,),
+    reason='The invert parameter is not supported for VTK<9. The general logic for the API differences is tested for VTK<9.1 though.',
+)
+def test_threshold_paraview_consistency():
+    """Validate expected results that match ParaView."""
+    x = np.arange(5, dtype=float)
+    y = np.arange(6, dtype=float)
+    z = np.arange(2, dtype=float)
+    xx, yy, zz = np.meshgrid(x, y, z)
+    mesh = pyvista.StructuredGrid(xx, yy, zz)
+    mesh.cell_data.set_scalars(np.repeat(range(5), 4))
+
+    # Input mesh
+    #   [[0, 0, 0, 0, 1],
+    #    [1, 1, 1, 2, 2],
+    #    [2, 2, 3, 3, 3],
+    #    [3, 4, 4, 4, 4]]
+
+    # upper(0): extract all
+    thresh = mesh.threshold(0, invert=False, method='upper')
+    assert thresh.n_cells == mesh.n_cells
+    assert np.allclose(thresh.active_scalars, mesh.active_scalars)
+    # upper(0),invert: extract none
+    thresh = mesh.threshold(0, invert=True, method='upper')
+    assert thresh.n_cells == 0
+
+    # lower(0)
+    #   [[0, 0, 0, 0   ]]
+    thresh = mesh.threshold(0, invert=False, method='lower')
+    assert thresh.n_cells == 4
+    assert np.allclose(thresh.active_scalars, np.array([0, 0, 0, 0]))
+    # lower(0),invert
+    #   [[            1],
+    #    [1, 1, 1, 2, 2],
+    #    [2, 2, 3, 3, 3],
+    #    [3, 4, 4, 4, 4]]
+    thresh = mesh.threshold(0, invert=True, method='lower')
+    assert thresh.n_cells == 16
+    assert thresh.get_data_range() == (1, 4)
+
+    # upper(2)
+    #   [[         2, 2],
+    #    [2, 2, 3, 3, 3],
+    #    [3, 4, 4, 4, 4]]
+    thresh = mesh.threshold(2, invert=False, method='upper')
+    assert thresh.n_cells == 12
+    assert thresh.get_data_range() == (2, 4)
+    # upper(2),invert
+    #   [[0, 0, 0, 0, 1],
+    #    [1, 1, 1,     ]]
+    thresh = mesh.threshold(2, invert=True, method='upper')
+    assert thresh.n_cells == 8
+    assert thresh.get_data_range() == (0, 1)
+
+    # lower(2)
+    #   [[0, 0, 0, 0, 1],
+    #    [1, 1, 1, 2, 2],
+    #    [2, 2,        ]]
+    thresh = mesh.threshold(2, invert=False, method='lower')
+    assert thresh.n_cells == 12
+    assert thresh.get_data_range() == (0, 2)
+    # lower(2),invert
+    #   [[      3, 3, 3],
+    #    [3, 4, 4, 4, 4]]
+    thresh = mesh.threshold(2, invert=True, method='lower')
+    assert thresh.n_cells == 8
+    assert thresh.get_data_range() == (3, 4)
+
+    # between(0, 0)
+    #   [[0, 0, 0, 0   ]]
+    thresh = mesh.threshold((0, 0), invert=False)
+    assert thresh.n_cells == 4
+    assert np.allclose(thresh.active_scalars, np.array([0, 0, 0, 0]))
+    # between(0,0),invert
+    #   [[            1],
+    #    [1, 1, 1, 2, 2],
+    #    [2, 2, 3, 3, 3],
+    #    [3, 4, 4, 4, 4]]
+    thresh = mesh.threshold((0, 0), invert=True)
+    assert thresh.n_cells == 16
+    assert thresh.get_data_range() == (1, 4)
+
+    # between(2,3)
+    #   [[         2, 2],
+    #    [2, 2, 3, 3, 3],
+    #    [3,           ]]
+    thresh = mesh.threshold((2, 3), invert=False)
+    assert thresh.n_cells == 8
+    assert thresh.get_data_range() == (2, 3)
+    # between(2,3),invert
+    #   [[0, 0, 0, 0, 1],
+    #    [1, 1, 1,     ],
+    #    [             ],
+    #    [   4, 4, 4, 4]]
+    thresh = mesh.threshold((2, 3), invert=True)
+    assert thresh.n_cells == 12
+    assert thresh.get_data_range() == (0, 4)
+
+
 def test_outline(datasets):
     for dataset in datasets:
         outline = dataset.outline(progress_bar=True)
@@ -911,6 +1024,10 @@ def test_glyph_orient_and_scale():
     assert glyph4.bounds[0] == geom.bounds[0] and glyph4.bounds[1] == geom.bounds[1]
 
 
+@pytest.mark.skipif(
+    pyvista.vtk_version_info < (9,),
+    reason='The invert parameter is not supported for VTK<9.',
+)
 def test_split_and_connectivity():
     # Load a simple example mesh
     dataset = examples.load_uniform()