Square pixels

Hello,

I am trying to create a 2D raster plot which expands to fill the available space but maintains square pixels. I believe match_aspect = True for figure should do this and it seems to work until the figure is included in row/col. Here is my example:

import numpy as np

from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import WheelZoomTool
from scipy import misc

img = misc.face(gray=True)
p = figure(width=400, height=400, tools=['wheel_zoom'], sizing_mode="scale_both", match_aspect = True)
p.toolbar.active_scroll = p.select_one(WheelZoomTool)
p.image(image=[img], x=0, y=0, dw=img.shape[0], dh=img.shape[1],palette= 'Cividis256')
#show(p)
show(row(column(p,height_policy='max', width_policy='max'),height_policy='max', width_policy='max'))

If you run this, zoom in with the wheel zoom and then resize the browser window, the face is distorted. Without the row/col, match_aspect works but with row/col the face is distorted when the browser window is resized.

I struggle to get complex layouts to resize and fill the desired space so I don’t know if this is a bug or not… I’d appreciate any suggestions anyone might have (and also some feedback on whether this seems like a bug or not).

Thanks in advance for any help… I’m using Bokeh 3.2.1

I believe sizing_mode="scale_both" is interfering with aspect matching. I am not sure offhand if this is a bug, or a fundamental incompatibility.

In any case, if I change to sizing_mode="stretch_both" instead, then the pixels remain square for both versions for me (because the plot ranges are able to adjust), in case that is an option.

Thanks very much for your reply Bryan… I could certainly switch to sizing_mode="stretch_both", but I do not see any difference. I made a video of my experience. Maybe this has been changed with a newer version of Bokeh? Or maybe it varies by browser… I use Chrome. The only change I made was to the sizing_mode. This is the updated script:

import numpy as np

from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import WheelZoomTool
from scipy import misc

img = misc.face(gray=True)
p = figure(width=400, height=400, tools=['wheel_zoom'], sizing_mode="stretch_both", match_aspect = True)
p.toolbar.active_scroll = p.select_one(WheelZoomTool)
p.image(image=[img], x=0, y=0, dw=img.shape[0], dh=img.shape[1],palette= 'Cividis256')
#show(p)
show(row(column(p,height_policy='max', width_policy='max'),height_policy='max', width_policy='max'))

I noticed that the original pixels are not square… this would still work for me if the aspect ratio of the pixels was preserved.

I didn’t initially appreciate the role of the zoom in this. match_aspect (as stated in its docs) only works when auto-ranging is active, and range bounds have not been set by the user. If you explicitly set range bounds, including by panning or zooming, then Bokeh will not override your setting, regardless of what happens to aspect ratios as a result. So I would regard this as behaving as expected. [1]

My statement above concerned resizing the plot without also zooming it, i.e. with auto-ranging still active. In that case, the aspect of the image is preserved even when the plot is resized (and this is accomplished by automatically updating the range bounds as necessary).

I don’t have a good option to suggest if you need to support zooming except to settle for a fixed sizing mode and not allow the plot to change size. Then the default zoom tool (and also box zoom with match_aspect=True set) will preserve the original aspect through their operation.

As for why the pixels are not square, it’s because of you have this backwards:

dw=img.shape[0], dh=img.shape[1]

Numpy arrays are row-major by default, so you want the reverse:

dw=img.shape[1], dh=img.shape[0]

With that change (and a fixed sizing mode) the pixels are square, as expected:


  1. Presumably aspect matching would start working again if you added and used a reset tool after a plot resize, because then auto-ranging would resume. ↩︎

Thank you again for your response and the bug fix for the example script, Bryan. I really appreciate the help. This response makes a lot of sense. It has helped me understand what previously seemed like a contradiction. Resizing the browser frame changes the aspect ratio of the pixels when:

show(row(column(p,height_policy='max', width_policy='max'),height_policy='max', width_policy='max'))

is used, but when:

show(p)

is used the display maintains square pixels, even when the browser window is resized. However, adding sizing_mode="scale_both" to row and column makes these two cases behave the same.