Skip to content

Tiling canvas overlays - #7836

Merged
brisvag merged 119 commits into
napari:mainfrom
brisvag:feature/tiling-overlays2
Sep 26, 2025
Merged

brisvag merged 119 commits into
napari:mainfrom
brisvag:feature/tiling-overlays2

Conversation

@brisvag

@brisvag brisvag commented Apr 17, 2025 •

Copy link
Copy Markdown
Contributor

References and relevant issues

Part of the work in #7832.
Depends on #7835.
Diff relative to #7835: brisvag/napari@refactor/overlays...brisvag:napari:feature/tiling-overlays2

Description

Canvas overlays can be positioned on the canvas using a limited set of positions (top_left, bottom_right and so on).
Currently, if two overlays end up in the same place, they simply overlap.

This PR introduces tiling for canvas overlays by looping through all canvas overlays and assigning an (horizontal) offset equal to the space taken up by all previous overlays.

import napari
v = napari.Viewer()
v.add_points()

v.scale_bar.visible = True

v.text_overlay.visible = True
v.text_overlay.text = 'Some interesting text'
v.text_overlay.position = 'bottom_right'

napari.run()

On main:
image

With this PR:
image

@brisvag
brisvag requested a review from melonora as a code owner April 17, 2025 13:58
@brisvag brisvag changed the title overlay base refactor Tiling canvas overlays Apr 17, 2025
@github-actions github-actions Bot added the tests Something related to our tests label Apr 17, 2025
@brisvag brisvag added the feature New feature or request label Apr 17, 2025
@brisvag brisvag added this to the 0.6.1 milestone Apr 17, 2025
@jni

jni commented Apr 18, 2025

Copy link
Copy Markdown
Member

Wow so cool @brisvag! 😍

@codecov

codecov Bot commented Apr 22, 2025 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.80519% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.04%. Comparing base (2554b31) to head (ca03e8b).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
src/napari/_vispy/utils/text.py 91.39% 8 Missing ⚠️
src/napari/_vispy/canvas.py 92.50% 6 Missing ⚠️
src/napari/_vispy/overlays/text.py 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7836      +/-   ##
==========================================
+ Coverage   92.98%   93.04%   +0.06%     
==========================================
  Files         703      704       +1     
  Lines       63365    63507     +142     
==========================================
+ Hits        58920    59091     +171     
+ Misses       4445     4416      -29     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TimMonko

Copy link
Copy Markdown
Member

Looks awesome!

How challenging would it be to add a vertical tiling option? This would generally be my preference for some things (like in the PR description). It's tough because it has to be generic enough for every canvas overlay, which might most often work nicely with horizontal (like colorbars).

But, just thinking it would be the adjustment between using the x_offsets vs y_offsets.

@brisvag

brisvag commented Apr 23, 2025

Copy link
Copy Markdown
Contributor Author

I agree that it would be cool; the problem with that is not the actual implementation (which is indeed pretty easy with y_offset) but the complexity of the logic of combining horizontal and vertical tiling.

Imagine a scenario where you have 4 overlays at the same corner.

Let's say we have vertical and horizontal tiling be a parameter for each overlay, and we have 2 with "horizontal" and 2 with "vertical". What do you expect to happen? I don't think there's a unique answer to that :P

But I would definitely like to explore this further, I'm just saying it's way more complex than simply sticking to a single direction, and should be done in a followup PR!

@brisvag

brisvag commented Apr 25, 2025

Copy link
Copy Markdown
Contributor Author

Alternatively, a simpler partial solution would be to have a per-position tiling option somewhere in the settings or the viewer itself, allowing to set something like overlays.top_left.orientation = 'vertical'. This would get better tiling for cases like the above, without having to deal with mixed tiling.

@TimMonko

TimMonko commented May 8, 2025

Copy link
Copy Markdown
Member

I agree that it would be cool; the problem with that is not the actual implementation (which is indeed pretty easy with y_offset) but the complexity of the logic of combining horizontal and vertical tiling.

Imagine a scenario where you have 4 overlays at the same corner.

Let's say we have vertical and horizontal tiling be a parameter for each overlay, and we have 2 with "horizontal" and 2 with "vertical". What do you expect to happen? I don't think there's a unique answer to that :P

But I would definitely like to explore this further, I'm just saying it's way more complex than simply sticking to a single direction, and should be done in a followup PR!

I was actually thinking of a hybrid of your two ideas. Sorry I didn't swing back to this much sooner!

Instead of the tiling logic being with the overlay itself, its tied to the canvas. Thus it becomes canvas.overlays.orientation= rather than canvas.overlays.scale_bar.orientation

Perhaps this waits until multi-canvas gets sorted and we go with this basic implementation for now. 😄

@brisvag

brisvag commented Sep 25, 2025

Copy link
Copy Markdown
Contributor Author

Ok, summary of all the tests:

On my machine (no desktop environment, just window manager):

  • default: 1, 1, 1
  • Scaling using QT_SCALE_FACTOR=2: 2, 1, 2
  • In all cases, everything looks fine

On @jni's mac:

  • default is 1, 1, 2
  • Scaling using OSX "more space" (:facepalm:): 1, 1, 2 (some hidden nonsense)
  • Scaling using QT_SCALE_FACTOR=2: 1, 1, 4 (wtf)

On @Czaki's mac (uses an app called "better display" for rescaling:

  • default is 1, 1, 1
  • Scaling using the "better display" slider: 2, 2, 2
  • in all cases using "better display", it results in the ugly overlap, depending on the font size

On @Czaki's windows:

  • default is 1, 1, 1
  • Every type of scaling results in 2, 2, 2 and similar, consistent changes across the board
  • EVERTYTHING LOOKS FINE (????)

On @TimMonko's windows machines:

  • crazy nonsense

Conclusions:

  • 😭
  • We probably need vispy top somehow detect some hidden thing that it's not currently detecting

@brisvag

brisvag commented Sep 25, 2025

Copy link
Copy Markdown
Contributor Author

Y'all, I think we got it!

To summarize:

  • most software actually assumes that "base dpi" is 96 (not 72..., though vispy says that, but it ultimately doesn't matter). This means that at a dpi of 96, a font size of 10 looks "as intended".
  • scaling has ultimately nothing to do with any of this, it just made the issue more inconsistent and harder to figure out
  • vispy rescales text based on dpi, but not the rest. So at double dpi, while font_size = 10 means 20, 10 pixels still means 10. This resulted in the text offset/padding being mismatched with the text size.
  • I added a new Text visual that abstracts this away, so we can just do Text.font_size and Text.get_width_height and not worry about it.
  • Tested this with @TimMonko and me and everything looks good. Would be good to get @Czaki confirmation as well, as he also had issues.

@TimMonko

Copy link
Copy Markdown
Member

gridded is currently not being accounted for correctly with tiling. See how text_overlay overlaps scale_bar. This only happens when scale_bar.gridded= True is true for one and False for the other. If both are gridded it calculates correctly.
import napari

v = napari.Viewer()

v.scale_bar.visible = True
v.scale_bar.box = True
v.scale_bar.gridded = True

v.text_overlay.visible = True
v.text_overlay.text = 'Points'
v.text_overlay.font_size = 20
v.text_overlay.position = 'bottom_right'

v.grid.enabled = True
v.grid.stride = 2

ll = v.open_sample('napari', 'lily')

napari.run()
image

@TimMonko

Copy link
Copy Markdown
Member

oh, and if its a struggled to figure out --- I want to be clear that my comment about the gridded text overlap with non-gridded is not a blocker. It's not a regression, because on main the same thing happens. I tried briefly to globally get the position of all overlays but only spent 15 minutes on it and got confused lol.
We can make follow-up PRs because this PR is already well behaved as long as nothing is gridded (which is new, and niche, anyways)

@brisvag

brisvag commented Sep 26, 2025 •

Copy link
Copy Markdown
Contributor Author

That's not a bug, it's the intended behaviour (at least, I intended it xD). Overlays are tiled on a by-viewbox basiss, and the "main viewbox" (where non-gridded stuff goes) lives layered on top of the gridded viewboxes, so there is no interaction between overlays across views.

If we wanted to get what you expect, it could only work with the corners (e.g: not with CanvasPosition.BOTTOM_CENTER), and for each corner we would have to:

  • get the viewbox at that corner in the grid
  • get the non-gridded overlays at that corner
  • treat them as if they were in the same viewbox for tiling purposes

Now that I write it out maybe it's not that hard... But yeah, let's do later xD

@brisvag brisvag added the ready to merge Last chance for comments! Will be merged in ~24h label Sep 26, 2025
@brisvag
brisvag merged commit 0fd1d9e into napari:main Sep 26, 2025
37 of 38 checks passed
@github-actions github-actions Bot removed the ready to merge Last chance for comments! Will be merged in ~24h label Sep 26, 2025
brisvag added a commit that referenced this pull request Sep 26, 2025
# References and relevant issues
- #5960
- #7587
- depends on #7836
- includes changes from #7835 and #7836. Relative diff:
brisvag/napari@brisvag:napari:feature/tiling-overlays2...feature/colormap-overlay


# Description
Added a colormap/colorbar overlay.

Try with:

```py
import napari

viewer = napari.Viewer()

# enable grid with stride 2 to get layers split two-by-two
viewer.grid.enabled = True
viewer.grid.stride = 2

# set the scale bar to gridded mode so it appears in each grid box
viewer.scale_bar.visible = True
viewer.scale_bar.gridded = True

layers = viewer.open_sample('napari', 'lily')

# enable color bars
for layer in layers:
    layer.color_bar.visible = True
```



![](https://private-user-images.githubusercontent.com/23482191/494350517-622b2d36-11a7-4c55-9550-c82ddebc2fda.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTg4ODg2MzIsIm5iZiI6MTc1ODg4ODMzMiwicGF0aCI6Ii8yMzQ4MjE5MS80OTQzNTA1MTctNjIyYjJkMzYtMTFhNy00YzU1LTk1NTAtYzgyZGRlYmMyZmRhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA5MjYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwOTI2VDEyMDUzMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWU4MzU3ODE5ZDE5MjBlOWVkYjE3MTM1MDk5ZjU1NWIwZDdkNDYxYTc5MzQ5NDc4YmJiNDVhZjg2NDI2NGYwN2ImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.5TtqoDkMICZEXdq99vCBlV4jGL2k_3uMt0FI2BjDwrc)

---------

Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>
Co-authored-by: Tim Monko <timmonko@gmail.com>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@imagesc-bot

Copy link
Copy Markdown

This pull request has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/napari-v0-6-5-is-out/116715/1

@brisvag brisvag mentioned this pull request Oct 8, 2025
brisvag added a commit that referenced this pull request Oct 9, 2025
# References and relevant issues
- closes #8342

# Description
This slipped through when refactoring in #7836. The root of the problem
is:
- canvas overlays can have "locked" position (top_left, bottom_right,
etc), or "free" positions (whatever the overlay wants)
- overlays need to opt out of the "locked" mode, which takes care of
tiling and auto-sets the position
- to opt out you need to specifically set `overlaymodel.position` to a
value that *isn't* a `CanvasPosition` enum
- you also should override the method `_on_position_change` in the
`VispyOverlay` class in order to avoid unnecessarily updating the tiling

I changed the naming scheme to adapt to this, and now everything works.
Basically what was happening is that the overlay was being shifted to
`bottom_right` automatically, which meant it was outside of the canvas
:P
snorkelopstesting3-bot pushed a commit to snorkel-marlin-repos/napari_napari_pr_7836_afa51493-f715-4dd8-a1bf-46157f726c42 that referenced this pull request Oct 22, 2025
Original PR #7836 by brisvag
Original: napari/napari#7836
snorkelopstesting4-web added a commit to snorkel-marlin-repos/napari_napari_pr_7836_afa51493-f715-4dd8-a1bf-46157f726c42 that referenced this pull request Oct 22, 2025
Merged from original PR #7836
Original: napari/napari#7836
DragaDoncila added a commit that referenced this pull request Feb 8, 2026
# References and relevant issues
- closes #7321
- supersedes/closes #6678
- depends on #7836
- depends on #8513

# Description
This PR refactors the `VispyTextOverlay` to be more easily reused
(following ideas from #6678). The result is that we can easily subclass
it to make custom text overlays that display different kinds of
information. I added two such examples: the `LayerNameOverlay` (useful
in grid mode) and the `CurrentSliceOverlay` (which shows the current
slice on the canvas. Both of the above are especially useful for
figure-making.

```py
import napari
v = napari.Viewer()
v.grid.enabled = True
ll = v.open_sample('napari', 'cells3d')
for l in ll:
    l.name_overlay.visible = True
v.scale_bar.visible = True
v.scale_bar.gridded = True
v._overlays['current_slice'].visible = True
v._overlays['current_slice'].gridded = True
v.dims.axis_labels = ['z', 'y', 'x']

```
<img width="1112" height="553" alt="image"
src="https://github.com/user-attachments/assets/15ccadfb-3651-4323-ab81-a2734979651c"
/>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>
Co-authored-by: napari-bot <napari-bot@users.noreply.github.com>
Co-authored-by: Tim Monko <timmonko@gmail.com>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
Co-authored-by: Draga Doncila Pop <17995243+DragaDoncila@users.noreply.github.com>
Co-authored-by: Peter Sobolewski <pete.sd@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request tests Something related to our tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants