Y_range not updating correctly in callback

Hi all,

I am setting the y_range.end of several plots in a callback based on the max y_range of the plots. This initially works, but after the data is updated, it works/doesn’t work erratically. I know that I can link the plots by setting p2.y_range = p1.y_range, but this is just a simplified example. Below is a minimal version of the script.

In the first image you can see that the y_ranges aren’t the same.

If you keep clicking the button, eventually they will sync again, but will unsync after clicking again.

Bokeh 1.4
Chrome 78.0.3904.97 (Official Build) (64-bit)
Windows 10 Pro

import numpy as np
import pandas as pd

from bokeh.io import curdoc
from bokeh.layouts import row, column
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Button


def initialize_data():

    x = np.arange(10)

    d1 = ColumnDataSource(pd.DataFrame({
        'x': x,
        'y': x
    }))

    d2 = ColumnDataSource(pd.DataFrame({
        'x': x,
        'y': x * 2
    }))

    return d1, d2


def initialize_figures(d1, d2, ymax):

    p1 = figure(sizing_mode='scale_width', tools='', toolbar_location=None, plot_width=4, plot_height=3)
    p1.circle('x', 'y', source=d1, color='red', size=15)
    p1.y_range.start = 0
    p1.y_range.end = ymax

    p2 = figure(sizing_mode='scale_width', tools='', toolbar_location=None, plot_width=4, plot_height=3)
    p2.circle('x', 'y', source=d2, color='blue', size=15)
    p2.y_range.start = 0
    p2.y_range.end = ymax

    return p1, p2


def update(attr, old, new):

    d1.data.update({'y': d1.data['y'] * 1.25})

    ymax = max([max(i.data['y']) * 1.1 for i in [d1, d2]])

    p1.y_range.end = ymax
    p2.y_range.end = ymax


d1, d2 = initialize_data()

ymax = max([max(i.data['y']) * 1.1 for i in [d1, d2]])

p1, p2 = initialize_figures(d1, d2, ymax)

r = row(p1, p2, sizing_mode='scale_width')

button = Button(label='PUSH', sizing_mode='scale_width', height=30, width=500)
button.on_change('clicks', update)

c = column(button, r, sizing_mode='scale_width')

curdoc().add_root(c)

(Note that the button callback signature and event is not correct above and had to be edited in order to run)

By default Bokeh uses and auto-ranging DataRange1d. If you are managing ranges explicitly as you are, thye just add unnecessary complications and opportunities for unwanted and unintended interactions. You should use a plain Range1d. There are a variety of ways to do that, but this is probably the simplest:

def initialize_figures(d1, d2, ymax):

    p1 = figure(sizing_mode='scale_width', tools='', toolbar_location=None,
                plot_width=4, plot_height=3, y_range=(0, ymax))
    p1.circle('x', 'y', source=d1, color='red', size=15)

    p2 = figure(sizing_mode='scale_width', tools='', toolbar_location=None,
                plot_width=4, plot_height=3, y_range=(0, ymax))
    p2.circle('x', 'y', source=d2, color='blue', size=15)

    return p1, p2

With this change the app performs as expected for me.

I’m not 100% but a quick guess is that because you’re not setting the y_range in the call for figure, it’s using an autorange which then keeps autoranging…

Bryan beat me to it :smiley:

JINX!

That worked! Thanks both of you, I didn’t realize setting the y_range like that would change it that much.

1 Like

It’s probably not obvious, but that is a convenience syntax to construct Range1d (you could also pass Range1d objects you construct yourself)