Updating y_range with CustomJS callback

Hi,
I plot a time serie, along with a WheelZoomTool to zoom in & out (on the x_axis).
I’m trying to auto scale the y_range based on the line plot displayed.

import bokeh
import numpy as np
import pandas as pd

import bokeh.plotting

time_series = pd.Series(np.random.random(1000) - .49).cumsum().to_frame('data').rename_axis('ts').reset_index()
source_ref = bokeh.plotting.ColumnDataSource(data=time_series)
source_disp = bokeh.plotting.ColumnDataSource(data=time_series)

""" Define tools """
pan = bokeh.models.PanTool(dimensions='width')
wheel = bokeh.models.WheelZoomTool(dimensions='width', maintain_focus=True)

figure = bokeh.plotting.figure(title='Performance Report',
                               x_range=bokeh.models.Range1d(time_series.index[0], time_series.index[-1],
                                                            min_interval=15,
                                                            bounds=(time_series.index[0],  # set limit to range
                                                                    time_series.index[-1])),
                               tools=[pan, wheel],
                               active_drag=pan,
                               active_scroll=wheel,
                               y_axis_label='Plot',
                               x_axis_label='Time')

figure.line(x='index', y='data', source=source_disp)

figure.x_range.js_on_change('end',
                            bokeh.models.CustomJS(args={
                                # 'figure': figure,
                                'y_range': figure.y_range,
                                'source': source_ref,
                                'source_disp': source_disp},
                                code=
                                """
                                let i = Math.max(Math.floor(cb_obj.start), 0);
                                let j = Math.min(Math.ceil(cb_obj.end), source.data['data'].length);
                                let max = Math.max.apply(null, source.data['data'].slice(i, j));
                                
                                const ref = source.data;
                                const data = source_disp.data;

                                data["index"] = ref["index"].slice(i, j);
                                data["ts"] = ref["ts"].slice(i, j);
                                data["data"] = ref["data"].slice(i, j);
                                source_disp.change.emit();

                                console.log("y_range end" + y_range.end);
                                y_range.end = max;
                                y_range.change.emit();
                                console.log("y_range end" + y_range.end);
                                console.log("end callback");
                                """
                            ))
bokeh.plotting.show(figure,
                    filename=filepath)

Here we can see that when I zoom in, the y_range scale does not adjust accordingly

When I try to run your code there is an error in the browsers JavaScript console:

TypeError: undefined is not an object (evaluating 'source.data['equity'].slice')

So first, things first, fix the example to not refer to an undefined column.

Hi Bryan,
Sorry, it was a typo from when I simplified the problem for more clarity.
I updated the code, with the correct column name.

I console.log the value of y_range.end before and after the emit.
It seems that in the JS code, the y_range.end is updated.
However, when I continue scrolling in/out, the y_range.end value keep the same, which means that the emit function did not propagated the update to the actual figure.y_range.
That is indeed what we observe, since the chart does not scale when zooming in/out.

Well, actually you need to describe the interaction you are looking for in detail. Do you mean scrolling the scroll zoom tool? Because by default that updates both ranges, so it would be competing / conflicting with anything in the JS callback. You would presumably want to restrict the scroll zoom to update only the x-axis and not touch the y-axis.

All that said, I am still a little confused by the behaviour and I am afraid I don’t have any immediate solutions for you. I will need to pare down the example more to investigate. Why are you slicing the CDS columns, for instance? What’s wrong with the full data being in the CDS, just part of it off-screen?

Hello Bryan,
When scrolling with the scroll zoom tool, by default, it zooms proportionately on both x & y axis.
What I’m trying to achieve, is to scroll on x_axis, and have y that auto scale in order to have a neat and accurate view of the data. So I set the scroll only on the x_axis (dimensions=‘width’), and I’m trying to make the change on the y_axis accordingly.

Here are couple of ideas that I implemented, but none was working for me:

  • I originally tried to slice the data, assuming bokeh would fit the y_axis based on the data.
    with only a subset of the data in source, I thought it would focus on only those data.
  • I tried to work with the “only_visible=True” parameters of the axis
  • I tried to set start & end to None, to force an autofit
  • I tried to pass the whole figure as arg to CustomJS, and update figure.y_range.start / end there

From what I can see, the only thing that i’m not able to do, is to have the “y_range.change.emit();” to actually propagatee the change to the plot. It just seems like y_range is set at the beginning when creating the figure, and immutable from then.

If you have any idea, that I could investigate, please let me know.

@woody I think your computations are off. I don’t have any more time to look at this just now, but here is a simplified (working) version that updates the y-range to the min/max of the viewport data on a button push. I think you can use it as a reference:

import numpy as np
from bokeh.models import Button, ColumnDataSource, CustomJS
from bokeh.plotting import column, figure, show

x = np.linspace(0, 20, 200)
y = x**2

source = ColumnDataSource(data=dict(x=x, y=y))

p = figure(tools="xpan")
p.line("x", "y", source=source)

button = Button()
button.js_on_click(
    CustomJS(args={"xr": p.x_range, "yr": p.y_range, "source": source}, code="""
        const i = Math.max(source.data.x.findIndex((x) => x > xr.start), 0)

        const j0 = source.data.x.findIndex((x) => x > xr.end)
        const j = j0 >= 0 ? j0 : source.data.x.length - 1

        const ysub = source.data.y.slice(i, j)

        yr.start = Math.min.apply(null, ysub);
        yr.end = Math.max.apply(null, ysub);
    """)
)

show(column(p, button))
1 Like

BTW you might want to trigger off of a RangesUpdate event instead of off of x_range.end. The former will provide a single combined event when any range change happens.

2 Likes

Bryan,
Thank you very much for your help. From your example, I was able to solve my problem.
It seems like triggering events from a range object (figure.x_range in my case) prevented to change the range value (y_range here).

I replaced it with

figure.js_on_event(bokeh.events.RangesUpdate, jscode)

and now it works great.
Thank you

1 Like