Image plot in bokeh with tight axes and matching aspect ratio
Asked Answered
O

1

11

I'm using bokeh 1.0.1 version inside a Django application and I would like to display microscopic surface images as zoomable image plots with a color-encoded height and colorbar. In principle this works, but I have problems to get plots with the correct aspect ratio only showing the image without space around.

Here is an example for what I want to achieve: The resulting plot should

  • show an image of random data having a width of sx=10 and a height of sy=5 in data space (image size)
  • have axes limited to (0,sx) and (0,sy), on initial view and when zooming
  • a square on the screen should match a square in data space, at least in initial view

For the image I just use random data with nx=100 points in x direction and ny=100 points in y direction.

Here is my first approach:

Attempt 1

from bokeh.models.ranges import Range1d
from bokeh.plotting import figure, show
from bokeh.models import LinearColorMapper, ColorBar
import numpy as np

sx = 10
sy = 5

nx = 100
ny = 100    
arr = np.random.rand(nx, ny)

x_range = Range1d(start=0, end=sx, bounds=(0,sx))
y_range = Range1d(start=0, end=sy, bounds=(0,sy))

# Attempt 1
plot = figure(x_range=x_range, y_range=y_range, match_aspect=True)

# Attempt 2
# plot = figure(match_aspect=True)

# Attempt 3
# pw = 400
# ph = int(400/sx*sy)
# plot = figure(plot_width=pw, plot_height=ph, 
#               x_range=x_range, y_range=y_range, match_aspect=True)

color_mapper = LinearColorMapper(palette="Viridis256", 
                                 low=arr.min(), high=arr.max())
colorbar = ColorBar(color_mapper=color_mapper, location=(0,0))

plot.image([arr], x=[0], y=[0], dw=[sx], dh=[sy],  
           color_mapper=color_mapper)
plot.rect(x=[0,sx,sx,0,sx/2], y=[0,0,sy,sy,sy/2], 
          height=1, width=1, color='blue')

plot.add_layout(colorbar, 'right')    
show(plot)

I've also added blue squares to the plot in order to see, when the aspect ratio requirement fails.

Unfortunately, in the resulting picture, the square is no square any more, it's twice as high as wide. Zooming and panning works as expected.

Attempt 2

When leaving out the ranges by using

plot = figure(match_aspect=True)

I'll get this picture. The square is a square on the screen, this is fine, but the axis ranges changed, so there is now space around it. I would like to have only the data area covered by the image.

Attempt 3

Alternatively, when providing a plot_height and plot_width to the figure, with a pre-defined aspect ratio e.g. by

pw = 800 # plot width
ph = int(pw/sx*sy)
plot = figure(plot_width=pw, plot_height=ph, 
              x_range=x_range, y_range=y_range, 
              match_aspect=True)

I'll get this picture. The square is also not a square any more. It can be done almost, but it's difficult, because the plot_width also comprises the colorbar and the toolbar.

I've read this corresponding blog post and the corresponding bokeh documentation, but I cannot get it working.

Does anybody know how to achieve what I want or whether it is impossible? Responsive behaviour would also be nice, but we can neglect that for now. Thanks for any hint.

Update

After a conversation with a Bokeh developer on Gitter (thanks Bryan!) it seems that it is nearly impossible what I want.

The reason is, how match_aspect=True works in order to make a square in data space look like a square in pixel space: Given a canvas size, which may result from applying different sizing_mode settings for responsive behaviour, the data range is then changed in order to have the matching aspect ratio. So there is no other way to make the pixel aspect ratio to match the data aspect ratio without adding extra space around the image, i.e. to extend the axes over the given bounds. Also see the comment of this issue.

Going without responsive behaviour and then fixing the canvas size beforehand with respect to the aspect ratio could be done, but currently not perfectly because of all the other elements around the inner plot frame which also take space. There is a PR which may allow a direct control of inner frame dimensions, but I'm not sure how to do it.

Okay, what if I give up the goal to have tight axes? This is done in "Attempt 2" above, but there is too much empty space around the image, the same space that the image plot takes.

I've tried to use various range_padding* attributes, e.g.

x_range = DataRange1d(range_padding=10, range_padding_units='percent')
y_range = DataRange1d(range_padding=10, range_padding_units='percent')

but it doesn't reduce the amount of space around the plot, but increases it only. The padding in percent should be relative to the image dimensions given by dh and dw.

Does anybody know how to use the range_padding parameters to have smaller axis ranges or another way to have smaller paddings around the image plot in the example above (using match_aspect=True)? I've opened another question on this.

Orlena answered 4/12, 2018 at 16:48 Comment(1)
Now there is maybe another way to accomplish what I want, since bokeh 1.2.0 is out and the large PR was implemented in version 1.1.0. If anybody knows how it is possible to control the inner frame dimensions (or just a link), I'd love to hear from you. Thanks.Orlena
B
5

Can you accept this solution (works with Bokeh v1.0.4) ?

from bokeh.models.ranges import Range1d
from bokeh.plotting import figure, show
from bokeh.layouts import Row
from bokeh.models import LinearColorMapper, ColorBar
import numpy as np

sx = 10
sy = 5

nx = 100
ny = 100
arr = np.random.rand(nx, ny)

x_range = Range1d(start = 0, end = sx, bounds = (0, sx))
y_range = Range1d(start = 0, end = sy, bounds = (0, sy))

pw = 400
ph = pw * sy / sx
plot = figure(plot_width = pw, plot_height = ph,
              x_range = x_range, y_range = y_range, match_aspect = True)

color_mapper = LinearColorMapper(palette = "Viridis256",
                                 low = arr.min(), high = arr.max())

plot.image([arr], x = [0], y = [0], dw = [sx], dh = [sy], color_mapper = color_mapper)
plot.rect(x = [0, sx, sx, 0, sx / 2], y = [0, 0, sy, sy, sy / 2], height = 1, width = 1, color = 'blue')

colorbar_plot = figure(plot_height = ph, plot_width = 69, x_axis_location = None, y_axis_location = None, title = None, tools = '', toolbar_location = None)
colorbar = ColorBar(color_mapper = color_mapper, location = (0, 0))
colorbar_plot.add_layout(colorbar, 'left')

show(Row(plot, colorbar_plot))

Result:

enter image description here

Bourg answered 19/3, 2019 at 11:7 Comment(4)
Thank you @Tony! Unfortenately the problem remains that also in this picture, the blue square in the middle is not a square on the screen, similar as in my "Attempt 3". I think the reason is the extra width of the colorbar. Since I need this for a scientific application, the aspect ratio should match as exact as possible.Orlena
I added ColorBar in another plot next to the first one. Is this work around OK?Bourg
Thanks again. It's a good idea and almost okay for my application - but, when I apply this solution, I can still see that it's not a square, on my screen the square has an aspect ratio of 29/30 (not 1). I accept this solution nevertheless, because I think there is no perfect one at the moment. There is an PR in bokeh, which will allow the control of inner frame dimensions. This is planned to be released in bokeh 1.1. Hopefully this can be done then.Orlena
OK. Until then you could try to apply a correction for the error (introduced by axis or toolbar) Please vote up if you find my help useful :)Bourg

© 2022 - 2024 — McMap. All rights reserved.