Labels feature highlight on hover - #103
Conversation
Shapes have had a hover highlight since deck's `autoHighlight` was turned on for them; labels have not, and the gesture reading differently on the two kinds is a gap users notice immediately. Deck cannot do this job for labels. A labels tile's picking colour covers the whole quad, so there is no per-label deck object for `picking_filterHighlightColor` to act on — enabling `autoHighlight` would light up the entire tile. The highlight is therefore resolved per FRAGMENT: the bitmask shader compares the sampled instance id against a `highlightedLabelId` uniform. A uniform rather than a bit in the colour LUT, specifically. The pointer moves constantly, and baking hover into the table would re-upload a texture that is megabytes for a large segmentation on every move — the anti-pattern the whole labels design exists to prevent. Tiles are never touched either way. On the vis side the hovered label is runtime render state on a ref plus a version counter (`setHoveredLabel`), never Render Stack config: it would be meaningless in a saved view, and gating the re-render on crossing INTO A DIFFERENT LABEL keeps motion within one label free. Points already carry their highlight this way. The tint prop is `labelHighlightColor`, not `highlightColor`. That name belongs to deck's own `Layer`, which defaults it to navy `[0, 0, 128, 128]` and feeds it to `autoHighlight`; a prop of that name is never absent, so the labels default could never win and every hover drew in deck's navy against a navy fill — the reason this looked like it did nothing at all. `labelsLayer.spec` now pins the value that reaches the tile sublayer, not just the plumbing. Verified in the browser against `test-fixtures/v0.7.2/blobs.zarr`, on both the single-scale and the multiscale (tiled) labels paths: the hovered label tints and outlines in yellow, the highlight follows the pointer between labels, and it clears on moving off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There are two hover implementations — `SpatialCanvasViewer` (headless/embedded) and `SpatialCanvas` (the full UI) — each with its own `handleHover`. The previous commit only touched the first, so the highlight worked in the headless demo I verified against and did nothing at all in the full UI, which is where real datasets are actually opened. `setHoveredLabel` was already on the renderer props there; nothing ever called it. Rather than copy the resolution into a second `handleHover`, both now call one `resolveHoveredLabel` in `featureTooltipHover.ts`. Duplication is what caused this, so a single implementation is the fix, not a tidier version of the same shape. The shared resolver reads the label id straight off the pick object instead of going through `getFeaturePickEvent`, which also builds a tooltip. The viewer already paid for that call for `onFeatureHover`; the full UI did not, and adding a second tooltip build per pointer move to a path that already resolves tooltips separately is not a cost worth paying for a hover tint. A layer-config check guards that the pick belongs to a labels layer. Verified against a real dataset (`1113PMDC1_human_01.zarr`, ~16k cell ids) in the full UI: hovering a cell tints it, and with `cell_labels` and `nuclei_labels` both enabled — two elements that genuinely share label id 13032 — only the layer under the cursor lights up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The changeset should carry the headline feature, not the implementation reasoning; the rationale belongs in the code and the layer-prop-flow guide, which already carry it. Same there: one sentence on why the hovered label is a uniform rather than a LUT entry, not three. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bespoke `labelHighlightColor` was the wrong conclusion from a correct diagnosis. Deck's `highlightColor` is not merely a name collision — it means the same thing: deck blends it over a highlighted fragment weighted by its own alpha (`picking_filterHighlightColor`), which is what the labels shader does, and the value is RGBA 0-255 on both sides. Shapes already set it for `autoHighlight`, so one prop now covers both kinds instead of one name per layer type. What actually broke was the defaulting, not the name. `highlightColor ?? DEFAULT` can never fall back, because deck's base `Layer.defaultProps` fills its navy in and the prop is therefore never absent. Declaring the labels default in the layer's own `defaultProps` is what overrides it — deck merges defaultProps down the prototype chain, child winning. As a bonus, `highlightColor` is on deck's composite -> sublayer forwarding list, so it would reach the tile sublayers even without the explicit threading kept here. The regression test is retargeted accordingly: it pins that the tint arriving at the bitmask sublayer is the labels yellow and NOT deck's navy, which is exactly the assertion that fails if the default is ever moved back to a use-site fallback. Verified in the full UI on 1113PMDC1_human_01.zarr with the H&E image beneath, so the pick also carries deck's `-#view-id#` suffix: the hovered cell tints yellow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (18)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Hovering a labels feature now highlights it, the way
autoHighlightalready highlights a shape. Follow-on to #95, which this depends on.Deck cannot do this job for labels. A labels tile's picking colour covers the whole quad, so there is no per-label deck object for
picking_filterHighlightColorto act on, and enablingautoHighlightwould light up the entire tile. The highlight is resolved per fragment instead: the bitmask shader compares the sampled instance id against ahighlightedLabelIduniform. A uniform rather than a LUT entry, because the pointer moves constantly and baking hover into the table would re-upload a texture that is megabytes for a large segmentation on every move.On the vis side the hovered label is runtime render state on a ref plus a version counter (
setHoveredLabel), never Render Stack config — it would be meaningless in a saved view, and gating the re-render on crossing into a different label keeps motion within one label free. Points already carry their highlight this way.Both canvas surfaces, one resolver (
5bcda68)SpatialCanvasViewer(headless/embedded) andSpatialCanvas(full UI) each own ahandleHover. The first version wired only the viewer, so the highlight worked in the headless demo and did nothing in the full UI — which is where real datasets are actually opened. Both now call oneresolveHoveredLabelinfeatureTooltipHover.ts; duplication is what caused this, so a single implementation is the fix rather than a tidier copy of it.The shared resolver reads the label id off the pick object instead of going through
getFeaturePickEvent, which also builds a tooltip. The viewer already paid for that call foronFeatureHover; the full UI did not, and adding a second tooltip build per pointer move for a hover tint is not worth it.The tint prop is deck's own
highlightColor(408963c)Not a bespoke name. Deck blends its
highlightColorover a highlighted fragment weighted by that colour's alpha, which is exactly what the labels shader does, and it is RGBA 0-255 on both sides — so the name carries a shared meaning, and one prop covers shapes and labels.The subtlety worth knowing when reviewing: the labels default must be declared in the layer's
defaultProps.props.highlightColor ?? MY_DEFAULTcannot work, because deck's baseLayer.defaultPropsfills its navy[0, 0, 128, 128]in and the prop is therefore never absent. That is what made an earlier revision draw every hover in deck's navy against a navy fill — looking exactly like the feature was dead.labelsLayer.specpins the value arriving at the bitmask sublayer, which is the assertion that fails if the default is ever moved back to a use-site fallback.Verification
Browser-verified, not just compiled:
test-fixtures/v0.7.2/blobs.zarron both labels paths, single-scale and multiscale/tiled: the hovered label tints and outlines, the highlight follows the pointer between labels, and clears on moving off.1113PMDC1_human_01.zarr, ~16k cell ids) in the full UI, including with the H&E image beneath — which routes through Viv, so picks carry deck's-#view-id#suffix thatnormalizeDeckLayerIdstrips.cell_labelsandnuclei_labelsboth enabled — two elements that genuinely share label id 13032 — only the layer under the cursor lights up.Workspace build, all tests (788),
lint:biome,lint:react(only the pre-existingPointsFeatureStatewarning) and the docs build are green.Reviewer notes
layer-prop-flow.mdx(render-path section plus two anti-pattern rows),headless-viewer.mdx,mdv-release-checklist.mdx.useLayerDatapublic-surface tripwire goes 17 -> 18 members forsetHoveredLabel; the docblock prose carries the count in words and is updated to match.🤖 Generated with Claude Code