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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions