Skip to content

interpolate_curve.pts_to_poststep (and pts_to_prestep) crash on empty input — np.zeros(2 * len(x) - 1) becomes np.zeros(-1) #6893

@johngu

Description

@johngu

ALL software version info

Python              :  3.13.13 (main, May 10 2026, 19:26:54) [Clang 22.1.3 ]
Operating system    :  Linux-6.17.0-22-generic-x86_64-with-glibc2.42

holoviews           :  1.22.1
bokeh               :  3.9.0
numpy               :  2.4.4
panel               :  1.9.0
param               :  2.4.0

Description of expected behavior and the observed behavior

Rendering a Curve with zero data points and interpolation="steps-post" (or "steps-pre") crashes with ValueError: negative dimensions are not allowed.

The root cause is in holoviews/operation/element.py. interpolate_curve.pts_to_poststep computes:

steps = np.zeros(2 * len(x) - 1)

For an empty input array (len(x) == 0), this becomes np.zeros(-1), which numpy rejects. The same 2 * len(x) - 1 pattern appears in pts_to_prestep. pts_to_midstep uses 2 * len(x) which is safer but the value-array dtype handling on empty input is still untested.

Expected: an empty Curve should render as an empty curve (no error).

Observed: ValueError: negative dimensions are not allowed.

This crashes any interactive workflow where a stream-driven operation (e.g. downsample1d masked to an x-window that contains zero rows) feeds an empty Curve into the step-interpolation path. In our use case (downsample1d + RangeX on tick-level financial data, with users zooming into sub-second windows), the empty Curve appears intermittently. The crash then propagates up through Stream.trigger's subscriber loop (holoviews/streams.py:213), which has no try/except, silently cancelling every subsequent subscriber's refresh — so any sibling plots in the same trigger pass stay frozen on stale data, even though their own downsample1d would have produced valid output. This makes the bug particularly nasty to diagnose: the visible symptom is a chart stuck on a zoomed-in sample with no error visible in the browser, while the actual exception fires once in the Python kernel for an unrelated empty-curve operation.

Complete, minimal, self-contained example code that reproduces the issue

import holoviews as hv

hv.extension("bokeh")

# An empty Curve with steps-post interpolation.
empty_curve = hv.Curve(([], []), "x", "y").opts(interpolation="steps-post")

# Force render → triggers interpolate_curve via the bokeh backend.
hv.render(empty_curve)

interpolation="steps-pre" reproduces identically. interpolation="linear" (the default) does not crash since it doesn't go through interpolate_curve.

Stack traceback and/or browser JavaScript console output

Traceback (most recent call last):
  ...
  File "holoviews/plotting/bokeh/chart.py", line 466, in get_data
    element = interpolate_curve(element, interpolation=self.interpolation)
  File "param/parameterized.py", line 6335, in __new__
    return inst.__call__(*args, **params)
  File "holoviews/core/operation.py", line 218, in __call__
    return element.apply(self, **kwargs)
  File "holoviews/core/accessors.py", line 200, in __call__
    new_obj = apply_function(self._obj, **inner_kwargs)
  File "holoviews/core/operation.py", line 212, in __call__
    return self._apply(element)
  File "holoviews/core/operation.py", line 141, in _apply
    ret = self._process(element, key)
  File "holoviews/operation/element.py", line 1142, in _process
    return element.map(self._process_layer, Element)
  File "holoviews/core/dimension.py", line 761, in map
    return map_fn(self) if applies else self
  File "holoviews/operation/element.py", line 1136, in _process_layer
    xs, dvals = INTERPOLATE_FUNCS[self.p.interpolation](x, dvals)
  File "holoviews/operation/element.py", line 1110, in pts_to_poststep
    steps = np.zeros(2 * len(x) - 1)
ValueError: negative dimensions are not allowed

Suggested fix

Early-return on empty input in pts_to_poststep, pts_to_prestep, and pts_to_midstep. Roughly:

@classmethod
def pts_to_poststep(cls, x, values):
    if len(x) == 0:
        empty_x = np.empty(0, dtype=x.dtype if hasattr(x, "dtype") else float)
        empty_vals = tuple(np.empty(0, dtype=cls._get_dtype(v)) for v in values)
        return empty_x, empty_vals
    # ... existing implementation ...

A related secondary concern — that Stream.trigger's subscriber loop has no per-subscriber error isolation, so this single crash silently freezes every later subscriber's plot — is filed separately as .

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions