Skip to content

Fix heatmap hover pick raising IndexError in the kernel handler#106

Merged
Alek99 merged 3 commits into
mainfrom
fix/heatmap-pick-indexerror
Jul 20, 2026
Merged

Fix heatmap hover pick raising IndexError in the kernel handler#106
Alek99 merged 3 commits into
mainfrom
fix/heatmap-pick-indexerror

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Hovering (or clicking) a heatmap in a notebook crashed the Python-side pick handler with an IndexError on every mouse move, spamming the cell output with tracebacks via ipywidgets' CallbackDispatcher. The chart kept rendering — only the hover readout died.

Root cause

pick() (python/xy/interaction.py) assumed the point-like invariant len(t.x.values) == t.n_points. Heatmap traces violate it: they store only the two outer grid edges as x/y while n_points is rows * cols, so the bounds guard passed and t.x.values[cell_index] indexed a 2-element array. The resulting IndexError isn't caught by channel.py's except (TypeError, ValueError), so it escaped the comm handler.

Fix

Grid traces (t.grid_shape is not None) now take a dedicated branch in pick() that returns the bin readout per dossier §17: trace, index, row, col, and color_value — the exact f64 z from the canonical grid (honoring §16's f64-hover rule; omitted for NaN cells). Out-of-range indices return None. The same branch fixes the identical crash in the click path.

Two deliberate choices:

  • x/y are omitted from the row. The client merge lets kernel fields override the local row, and heatmap axes can be categorical (labels live figure-side while trace columns are numeric) — a kernel numeric x would replace category labels with raw numbers in the tooltip. The client's local row already shows the correct cell center; the kernel's genuine contribution is the exact z value. A test pins the omission.
  • Not suppressed with row: None. The client hides the tooltip on a null pick_result row, which would have erased the working local readout.

Testing

  • New: test_pick_heatmap_cell_returns_grid_readout, test_pick_heatmap_out_of_range_returns_none, test_pick_heatmap_nan_cell_omits_value (tests/test_scatter.py) and test_heatmap_pick_message_replies_without_raising (tests/test_channel.py, mirrors the heatmap+contour repro through handle_message).
  • uv run pytest tests/test_scatter.py tests/test_channel.py -q — 97 passed
  • uv run pytest tests/ -q -k "pick or heatmap" — 57 passed, 1 skipped
  • pre-commit run --all-files, ruff check, ruff format --check — clean

Closes #89

pick() assumed point-like traces where len(t.x) == n_points, but heatmap
traces store only the two outer grid edges while counting rows*cols
points, so every hover (and click) on a heatmap cell indexed a 2-element
array with a cell index and crashed the comm handler, spamming notebook
output with tracebacks.

Grid traces now take a dedicated branch that returns the bin readout of
dossier §17: row/col plus the exact f64 z from the canonical grid
(omitted for NaN cells). x/y are deliberately left out — the client's
local row already shows the cell center with categorical axis labels,
and kernel-provided numerics would override those labels in the tooltip
merge. Returning None instead was not an option: the client hides the
tooltip on a null pick_result row.
@codspeed-hq

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 94 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing fix/heatmap-pick-indexerror (3158eb6) with main (d3e9dfe)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@FarhanAliRaza FarhanAliRaza added the valid Issue is validated. This pr actually fixes the issue. Needs Code Review. label Jul 20, 2026
@Alek99
Alek99 merged commit 914a327 into main Jul 20, 2026
21 checks passed
Alek99 added a commit that referenced this pull request Jul 21, 2026
The move in this PR promoted docs/engineering/ to spec/ and CLAUDE.md
declares that tree the source of truth. An audit of all 24 documents
against the code found the tree had drifted at PR #96: five behavior
PRs (#103, #105, #106, #108, #115) had landed with no spec update, and
~100 claims contradicted shipped code.

Accuracy: correct claims that no longer match the implementation,
including the Rust ABI version (v31 -> v36), the "zero-crate cdylib"
claim (png is a real dependency), the pyramid ABI function names, the
panic-shield status, the R2/VAO claim in the renderer doc, the
WASM/Web-Worker architecture in dossier §3/§8 (superseded by §32's
C-ABI cdylib), the count-only tier rule, the matplotlib shim overhead
figures (the cited guardrail says +60% at 10k, not +9%), and the
Plotly Scattergl 10M figure that appeared as two different numbers
from the same run.

Coverage: add spec/api/export.md, spec/api/interaction.md and
spec/design/wire-protocol.md for three surfaces that had no coverage
at all, plus the config.py threshold table, full JS and Rust module
inventories, and the hexbin centers-only wire contract that three
renderers must expand identically.

Structure: group 15 top-level files into api/, design/, matplotlib/,
benchmarks/ and process/, and rewrite spec/README.md as a real index
(it previously listed only the two SVG assets, leaving 21 of 22 docs
unreachable). All 150 consumer references repointed, including the CI
--emit-md target and the compat-matrix generator.

Guard: verify_sdist.py pinned only 5 top-level spec files, so the
sdist could silently lose spec/design/ or spec/assets/ and still
pass; it now asserts markdown under every subdirectory and both SVGs.
Alek99 added a commit that referenced this pull request Jul 21, 2026
…roup the tree (#122)

* Move engineering docs to root spec directory

* Resync spec/ with the implementation and group the tree

The move in this PR promoted docs/engineering/ to spec/ and CLAUDE.md
declares that tree the source of truth. An audit of all 24 documents
against the code found the tree had drifted at PR #96: five behavior
PRs (#103, #105, #106, #108, #115) had landed with no spec update, and
~100 claims contradicted shipped code.

Accuracy: correct claims that no longer match the implementation,
including the Rust ABI version (v31 -> v36), the "zero-crate cdylib"
claim (png is a real dependency), the pyramid ABI function names, the
panic-shield status, the R2/VAO claim in the renderer doc, the
WASM/Web-Worker architecture in dossier §3/§8 (superseded by §32's
C-ABI cdylib), the count-only tier rule, the matplotlib shim overhead
figures (the cited guardrail says +60% at 10k, not +9%), and the
Plotly Scattergl 10M figure that appeared as two different numbers
from the same run.

Coverage: add spec/api/export.md, spec/api/interaction.md and
spec/design/wire-protocol.md for three surfaces that had no coverage
at all, plus the config.py threshold table, full JS and Rust module
inventories, and the hexbin centers-only wire contract that three
renderers must expand identically.

Structure: group 15 top-level files into api/, design/, matplotlib/,
benchmarks/ and process/, and rewrite spec/README.md as a real index
(it previously listed only the two SVG assets, leaving 21 of 22 docs
unreachable). All 150 consumer references repointed, including the CI
--emit-md target and the compat-matrix generator.

Guard: verify_sdist.py pinned only 5 top-level spec files, so the
sdist could silently lose spec/design/ or spec/assets/ and still
pass; it now asserts markdown under every subdirectory and both SVGs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

valid Issue is validated. This pr actually fixes the issue. Needs Code Review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants