Skip to content

Hovering a heatmap crashes the kernel-side pick handler (IndexError on 2-element edge array) #89

Description

@Darksinian

Summary

Hovering over a heatmap trace in a notebook raises an IndexError in the Python-side pick handler on every mouse move, spamming the cell output with repeated tracebacks. The chart keeps rendering — only the hover readout dies.

File python/xy/channel.py:191, in handle_message(...)
    row = fig.pick(trace_id, index, drill_seq, ...)
File python/xy/interaction.py:97, in pick
    "x": float(t.x.values[idx]),
IndexError: index 34280 is out of bounds for axis 0 with size 2

Repro

import numpy as np
import xy

gv = np.linspace(-3, 3, 200)
gx, gy = np.meshgrid(gv, gv)
z = np.exp(-(gx**2 + gy**2) / 2) - 0.6 * np.exp(-((gx - 1.2) ** 2 + (gy - 0.8) ** 2) / 0.5)

xy.chart(
    xy.heatmap(z, x=gv, y=gv, colormap="magma"),
    xy.contour(z, x=gv, y=gv, levels=8),
    width=900,
    height=420,
)

Render in Jupyter, then move the mouse across the heatmap → an IndexError traceback per hover event, surfaced through ipywidgets CallbackDispatcher / FigureWidget._on_custom_msg.

Root cause

pick() (python/xy/interaction.py:64) assumes a point-like trace layout where len(t.x.values) == t.n_points:

  1. The client GPU-picks the heatmap cell index under the cursor (0..rows*cols) and sends a pick message.
  2. The bounds guard idx < t.n_points passes, because for heatmap traces n_points comes from count = z_flat.size (_trace.py:64) — 40,000 for a 200x200 grid.
  3. But heatmap traces store only the two outer grid edges as x/y: marks.py ingests np.array([x_edges[0], x_edges[-1]]) (and same for y). So t.x.values[idx] at interaction.py:97 indexes a 2-element array with a cell index → IndexError.

The len(t.x) == n_points invariant holds for scatter/line but not for grid-shaped traces, and nothing filters grid traces out of the pick path — neither the client nor the kernel handler.

Suggested fix

In pick(), branch on grid traces (t.grid is not None): translate the flat cell index via grid_shape into row/col, read the cell value from t.grid, and derive cell-center x/y from the stored edges + shape. Minimal don't-crash fallback: return None for traces whose x/y length doesn't match n_points. (§17 exact-readout semantics suggest the full fix — a heatmap hover readout of the cell value is genuinely useful.)

Environment: Linux, Jupyter (ipywidgets transport), xy editable install of main @ 541a7da, Python 3.14, native kernel backend.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions