Repository: holoviz/holoviews

Issue:
Update contour line calculations to use ContourPy's LineType.ChunkCombinedNan
The upcoming 1.2 release of ContourPy will include a data format for contour lines tailored to how Bokeh and HoloViews accept lines as NaN-separate numpy arrays (https://github.com/contourpy/contourpy/pull/296). When this release is out on PyPI and conda-forge, we need to update HoloViews to use this new `LineType.ChunkCombinedNan` if `contourpy >= 1.2` is installed. This will improve the performance of calculating contour lines (strictly speaking the calculation takes about the same amount of time but the approach avoids allocating new numpy arrays to insert the NaN into).

Code changes required (wrapped in a version check) in `holoviews/operation/element.py`:

- The `line_type` in the `contour_generator` call.
- Handling the returned data from the `ContourGenerator.lines()` call.


Discussion:

Resolving patch:
diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py
--- a/holoviews/operation/element.py
+++ b/holoviews/operation/element.py
@@ -561,7 +561,12 @@ class contours(Operation):
 
     def _process(self, element, key=None):
         try:
-            from contourpy import FillType, LineType, contour_generator
+            from contourpy import (
+                FillType,
+                LineType,
+                __version__ as contourpy_version,
+                contour_generator,
+            )
         except ImportError:
             raise ImportError("contours operation requires contourpy.")
 
@@ -624,19 +629,36 @@ def _process(self, element, key=None):
         if self.p.filled:
             vdims = [vdims[0].clone(range=crange)]
 
+        if Version(contourpy_version) >= Version('1.2'):
+            line_type = LineType.ChunkCombinedNan
+        else:
+            line_type = LineType.ChunkCombinedOffset
+
         cont_gen = contour_generator(
             *data,
-            line_type=LineType.ChunkCombinedOffset,
+            line_type=line_type,
             fill_type=FillType.ChunkCombinedOffsetOffset,
         )
 
+        def coords_to_datetime(coords):
+            # coords is a 1D numpy array containing floats and possibly nans.
+            # Cannot pass nans to matplotlib's num2date.
+            nan_mask = np.isnan(coords)
+            any_nan = np.any(nan_mask)
+            if any_nan:
+                coords[nan_mask] = 0
+            coords = np.array(num2date(coords))
+            if any_nan:
+                coords[nan_mask] = np.nan
+            return coords
+
         def points_to_datetime(points):
             # transform x/y coordinates back to datetimes
             xs, ys = np.split(points, 2, axis=1)
             if data_is_datetime[0]:
-                xs = np.array(num2date(xs))
+                xs = coords_to_datetime(xs)
             if data_is_datetime[1]:
-                ys = np.array(num2date(ys))
+                ys = coords_to_datetime(ys)
             return np.concatenate((xs, ys), axis=1)
 
         paths = []
@@ -684,11 +706,14 @@ def points_to_datetime(points):
                 if any(data_is_datetime[0:2]):
                     points = points_to_datetime(points)
 
-                offsets = lines[1][0]
-                if offsets is not None and len(offsets) > 2:
-                    # Casting offsets to int64 to avoid possible numpy UFuncOutputCastingError
-                    offsets = offsets[1:-1].astype(np.int64)
-                    points = np.insert(points, offsets, np.nan, axis=0)
+                # If line_type == LineType.ChunkCombinedNan then points are already in
+                # the correct nan-separated format.
+                if line_type == LineType.ChunkCombinedOffset:
+                    offsets = lines[1][0]
+                    if offsets is not None and len(offsets) > 2:
+                        # Casting offsets to int64 to avoid possible numpy UFuncOutputCastingError
+                        offsets = offsets[1:-1].astype(np.int64)
+                        points = np.insert(points, offsets, np.nan, axis=0)
                 geom = {
                     element.vdims[0].name:
                     num2date(level) if data_is_datetime[2] else level,


Test patch: