1

I'm trying to make it so mousewheel zoom only zooms the x-axis, and the y-axis will automatically rescale to fill available whitespace. Plot:

enter image description here

And then I zoom in, there is unwanted whitespace:

enter image description here

How do I get the y-axis to automatically rescale?

from bokeh.plotting import figure, curdoc
from bokeh.driving import linear
from bokeh.models.tools import PanTool, WheelZoomTool

p = figure(sizing_mode="stretch_both", y_axis_location="right", x_axis_type="datetime")
pan_tool = p.select(dict(type=PanTool))
pan_tool.dimensions="width"
zoom_tool = p.select(dict(type=WheelZoomTool))
zoom_tool.dimensions="width"
p.x_range.follow = "end"
p.x_range.range_padding = 0.01
p.xaxis.ticker.desired_num_ticks = 8
r1 = p.step(range(3000), range(3000), color="red", line_width=0.4)
curdoc().add_root(p)

To run it:

bokeh serve --show bokeh_test.py
Jase
  • 1,025
  • 1
  • 9
  • 34
  • 1
    There is nothing built-in to rescale based on the viewport (there is still an [open feature request](https://github.com/bokeh/bokeh/issues/8913) on GIthub). You would need to add a `CustomJS` callback for x-range update events, and compute and set the y-range `start` and `end` based on your requirements. – bigreddot May 20 '22 at 18:50

2 Answers2

1

In order to automatically rescale the y-axis of a Bokeh plot based on the data in the plot, you can use a JavaScript callback function defined by CustomJS. This function takes two arguments: args, a dictionary of objects to make available to the JavaScript code, and code, the JavaScript code that will be executed when the callback is triggered.

In the code of the callback function, you can calculate the minimum and maximum values of the low and high data columns of your data source within the current x-range of the plot. Then, you can set the start and end values of the y_range object to be a percentage padding around the minimum and maximum values.

Finally, you can use the js_on_change method on the x_range object of the plot to add the callback function as a JavaScript callback that will be triggered when the start and end values of the x_range object change.

Overall, this solution provides a way to automatically rescale the y-axis of a Bokeh plot based on the data in the plot, which can be useful for visualizing data that may change dynamically over time

# JavaScript callback function to automatically zoom the Y axis to
callback = CustomJS(args={'y_range': main.y_range, 'source': source}, code='''

    clearTimeout(window._autoscale_timeout);
    var Index = source.data.time, Low = source.data.low, High = source.data.high, start = cb_obj.start, end = cb_obj.end, min = Infinity, max = -Infinity;

    for (var i=0; i < Index.length; ++i) {
         if (start <= Index[i] && Index[i] <= end) {
             max = Math.max(High[i], max); min = Math.min(Low[i], min);
         } 
    } 

    var pad = (max - min) * .09;
    window._autoscale_timeout = setTimeout(function() {
         y_range.start = min - pad; y_range.end = max + pad; 
    });

    '''.replace('\n', '').replace('\t','')
    )

main.x_range.js_on_change('start', callback)
main.x_range.js_on_change('end', callback)

enter image description here

0

Although this is an old question I put down my answer which is not completely solving the problem.

I hope someone can pick it up and fix what's wrong.

It seems to work just fine in other conditions; in this case though everything works fine until you zoom in too much and axis update seems to freeze until you reset the plot.

Please note this solution is an adaption of this bokeh issue

Here's below the code based on CustomJS callback on y_range 'start' event.

Also, I could not find a list of events for Range; maybe someone can point me to a reference.

from bokeh.plotting import figure, curdoc, show
from bokeh.driving import linear
from bokeh.models.tools import PanTool, WheelZoomTool
from bokeh.models import ColumnDataSource, CustomJS


yrange_update_callback = """
var plot = plot;

var data = source.data;
var x = source.data['x'];
var y = source.data['y'];
//console.log(data);
//console.log(plot);

console.log(cb_obj);
console.log(cb_data);
//x = data['x'];
//y = data['y'];

var Xstart = plot.x_range.start;
var Xend = plot.x_range.end;


var currentdate = new Date(); 
console.log(currentdate);
console.log(Xstart);
console.log(Xend);

function sGE (e) { return e >= Xstart; } 
function eGE (e) { return e >= Xend; } 
function fixstart (i) { return i >= 0 ? i : x.length - 20; } 
function fixend (i) { return i > 0 ? i : x.length - 1; } 

var Istart = fixstart(x.findIndex(sGE));
var Iend = fixend(x.findIndex(eGE));

console.log(Istart);
console.log(Iend);

var yview = y.slice(Istart, Iend+1);
var yv = [Math.min(...yview),Math.max(...yview)];
var dy = yv[1] - yv[0];

console.log(dy);

cb_obj.start = yv[0]-0.15*dy;
cb_obj.end = yv[1]+0.15*dy;
"""

p = figure(sizing_mode="stretch_both", y_axis_location="right")
pan_tool = p.select(dict(type=PanTool))
pan_tool.dimensions="width"
zoom_tool = p.select(dict(type=WheelZoomTool))
zoom_tool.dimensions="width"
p.x_range.follow = "end"
p.x_range.range_padding = 0.01
p.xaxis.ticker.desired_num_ticks = 8

source = ColumnDataSource({'x': list(range(3000)), 'y': list(range(3000))})

r1 = p.step(x='x', y='y', source=source, color="red", line_width=0.4)

CJS = CustomJS(args=dict(source=source, plot=p), code=yrange_update_callback)
p.y_range.js_on_change('start', CJS)

curdoc().add_root(p)
#show(p)
GDN
  • 196
  • 1
  • 8