Annulus/Annular Wedge Radius inconsistent

This is highly related to this → Plot size and circle radius , and yet another continuation of trying to implement this → Use DataRange1d to compute plot bounds? - #15 by gmerritt123

What I’m trying to do

Plot an annulus glyph (or annular wedge or circle etc.) with a dynamic (i.e. updating in browser) inner/outer radius, AND do so with specified Range1d start/stop values (which do get adjusted depending on the extent as per the second link thread).

What’s happening

The annulus glyph plots a “visual” circle regardless of plot dimensions (i.e. inner/outer width from what I can tell). This results in the “radius” not being the same in the X and Y dimensions in data-space:

Since I’ve spec’d my figure height and width as 600 and 800 respectively here, and the annulus is “forced” to always visually be a circle, the radius in the Y dimension goes well beyond 200.

match_aspect doesn’t change anything since I’ve got spec’d x/y ranges with a Range1d. And as per the first linked thread, the solution may be to resort to drawing patches/polygons (which i was trying sooo hard to avoid doing…).

And radius_dimension doesn’t even exist as an arg for Annulus… not that I think it’d solve my problem but I can’t even fiddle with it to investigate.

An alternative that’d work as well: set the plot’s inner height and width to ALWAYS match, so that all this becomes a non issue. Basically I just want a perfectly square plot area so I don’t even have to think about all of the above. Are there any tricks to accomplishing this?

FYI, for my use case, it’s not as easy as simply spec-ing height = 600, width = 600, as my plot a) has a colorbor layout on it and b) has dynamic axis labels/titles etc. that will affect the computer inner height and width…

MRE:

from bokeh.plotting import figure,show
from bokeh.models import Range1d


f = figure(height=600,width=800
            # ,match_aspect=True #doesn't change anything
           , title = 'Annulus with outer radius "allegedly" = 200'
           )

f.x_range = Range1d(start=-250,end=250)
f.y_range = Range1d(start=-250,end=250)

f.annulus(x=[0],y=[0],outer_radius=[200],inner_radius=[0],radius_dimension='max')

show(f)

Aspect-matching can only work when data ranges are used. That is because the way aspect-matching functions is by updating the ranges to achieve the desired aspect. If you use fixed, explicit Range1d as above, then Bokeh will not override you, i.e. you’ve told Bokeh to ignore match_aspect in preference to your values.

An older approach to aspect matching, before match_aspect was added, was to set the min_border values in a way that tried to control the pixel dimensions of the inner plot area. E.g. if your canvas is 600x600 and you set min_border=50 and make sure that things like tick labels will never exceed that 50px width, then the inner plot area should be 500x500. This is obviously a bit finicky and delicate but might also be worth a quick look. You can also control the min-border on each side separately if need be, say to accommodate bigger elements on one side.

Thanks,

This →

That is because the way aspect-matching functions is by updating the ranges to achieve the desired aspect. If you use fixed, explicit Range1d as above, then Bokeh will not override you, i.e. you’ve told Bokeh to ignore match_aspect in preference to your values.

made the lightbulb go off :smiley:

What I needed to do was calculate the computed aspect ratio in the callback that updates the range , so I can identify the “limiting” dimension and pad the non-limiting dimension accordingly, such that match-aspect gets maintained.

So basically, if the plot’s inner_height is less than the inner_width, the limiting dimension is y, and I need to set my y-range exactly as desired, and then proportionally pad my x range. And if plot’s inner_height is greater than the inner_width, the limiting dimension is now x, and i need to set x-range exactly as desired, and pad the y_range proportionally.

MRE Solution for posterity →

from bokeh.plotting import figure,show
from bokeh.models import Range1d, CustomJS, Slider
from bokeh.layouts import column

f = figure(height=600,width=1200
            # ,match_aspect=True #doesn't change anything
           , title = 'Annulus'
           )

f.x_range = Range1d(start=-250,end=250)
f.y_range = Range1d(start=-250,end=250)

r = f.annulus(x=[0],y=[0],outer_radius=[0],inner_radius=[0])

sl = Slider(start = 0, end = 1000, step=5,value=0,title='radius')

cb = CustomJS(args=dict(sl=sl,src=r.data_source,xr=f.x_range,yr=f.y_range, f=f)
              ,code='''
              src.data['outer_radius'][0] = sl.value
              src.change.emit()
              //get aspect ratio of current plot
              var ar = f.inner_height/f.inner_width
              console.log(ar)
              
            //update ranges to fit
            //if height<width, want to set yr to radius, and pad xr 
            if (ar<1){
                yr.start = -sl.value
                yr.end = sl.value
                xr.start = -sl.value/ar
                xr.end = sl.value/ar 
                }
            //if width<height, want to set xr to radius, and pad yr
            else {
                xr.start = -sl.value
                xr.end = sl.value
                yr.start = -sl.value*ar
                yr.end = sl.value*ar
                }            
              '''
              )
sl.js_on_change('value',cb)

show(column([f,sl]))
1 Like