Reset zoom after data source update from Bokeh Server?

I have a figure being displayed from a Bokeh server. The server has a callback that replaces the data in the figure with new data. After this update, the figure retains its original zoom (which in my case almost always results in a blank plot). Is there a way to reset the zoom on the figure from the Bokeh server? Or a setting that results in the zoom being reset on every data source update? Thank you for your time!

You can reset start and end attributes of the changes whenever the relevant data sources change. Either on the server, along with the data change, or with a js_on_change callback.

Hi, @p-himik. Could you clarify how to reset the start and end attributes (on the server side of things)?

My figure’s x_range and y_range properties appear to be DataRange1Ds. The Range1D class’ x_range and y_range appear to have reset_start and reset_end methods, but DataRange1D does not seem to.

I had also attempted setting the value of figure.x_range.start directly based on data_source.data[column_name]. However, this doesn’t include the default view padding Bokeh would normally apply, and requires knowing the column name ahead of time rather than basing it on what the original range was based on.

I meant that you can set start and end manually. You can even do it in a single operation: x_range.update(start=0, end=1). Other than that, I don’t have any ideas.

After some more experimentation, I believe this may be a bug. Below is a minimal example case (running bokeh serve example.py. A figure is shown with an initial plot. Then different things can happen.

Case 1:
The user does not update the view, but only clicks the button. We don’t have the server change the range values. In this case, the view automatically updates to fit the updated data.

Case 2:
The user does update the view before clicking the button. We don’t have the server change the range values. In this case, the view does not automatically update to fit the updated data and shows the manually selected view. The reset button on the client side will fit the view to the new data.

Case 3:
The user does not update the view, but only clicks the button. We have the server update the range ends to specific non-None values. In this case, the view automatically updates to fit the updated data. The range values we set are ignored.

Case 4:
The user updates the view and clicks the button. We have the server update the range ends to specific non-None values. In this case, the view is set to what we set the range values to be on the server side. The reset button auto fits the new data.

Case 5:
The user does not update the view, but only clicks the button. We have the server update the range ends to None. In this case, the view automatically updates to fit the updated data.

Case 6:
The user updates the view and clicks the button. We have the server update the range ends to None. In this case, the view shows nothing. The reset button auto fits the new data.

The two cases that seem to work contrary to what I would expect are cases 3 and 6. For case 3, I would expect that the range setting would override the auto data fitting. For case 6, I would expect setting the range ends to None to override the manual fit chosen, and re-enable auto fitting. Notably, these range changes are used by the client side in cases 4 and 5, so it doesn’t have to do with the client not getting the updates from the server. It seems more to be a problem with the server not being able to change whether or not the auto fitting is enabled on the client. Is there some way to do this that I’m missing? Is this a bug? Or is this the expected behavior? Thank you for your time!

example.py

import numpy as np
import pandas as pd
from bokeh.models import Button
from bokeh.plotting import Figure, curdoc
from bokeh.layouts import column
from bokeh.models.sources import ColumnDataSource

def dashboard():
    data0 = pd.DataFrame({'y': [1, 2, 3] * 5, 'x': np.arange(15)})
    data1 = pd.DataFrame({'y': [4, 5, 6] * 5, 'x': np.arange(15) + 1})

    data_source = ColumnDataSource(data0)

    button = Button()

    figure = Figure()
    figure.line('x', 'y', source=data_source)

    def updata_data():
        new_data = ColumnDataSource.from_df(data1)
        data_source.data.update(**new_data)
        # figure.x_range.start = None
        # figure.x_range.end = None
        # figure.y_range.start = None
        # figure.y_range.end = None

    button.on_click(updata_data)

    curdoc().add_root(column(button, figure))

dashboard()

You’re hitting this: Race condition when updating data source and x_range · Issue #7281 · bokeh/bokeh · GitHub
Also see Issue updating DataRange start/end explicitly · Issue #4014 · bokeh/bokeh · GitHub

Hmm, this is in some ways similar, but also kind of the opposite problem. The issuers here wanted to explicitly set ranges of the plot. I don’t want to explicitly set anything. The only reason I was considering explicitly setting the values is because it’s not automatically updating. After the user of the plot changes the view, the plot is no longer automatically updated on a data change. As mentioned in the second link, DataRange1D's “purpose is explicitly to automatically set start/end without intervention”. However, this is not happening in the case described above. But thank you, these are both certainly relevant.

This issue (which is linked from the issue you mentioned) suggests this is a bug. Here it was noted that the auto-ranging should be re-enabled when the range ends are set to None.

As a current work around, I’m trying to use the JS reset of the figure on the change of the data source. However, this also fails. When trying to add the js_on_change, the figure does not load. No error is given from Python and the javascript hands back TypeError: undefined is not an object (evaluating 'n.connect'). I’m using the data source js_on_change as noted here. Any thoughts there?

Code:

import numpy as np
import pandas as pd
from bokeh.models import Button, CustomJS
from bokeh.plotting import Figure, curdoc
from bokeh.layouts import column
from bokeh.models.sources import ColumnDataSource

def dashboard():
    data0 = pd.DataFrame({'y': [1, 2, 3] * 5, 'x': np.arange(15)})
    data1 = pd.DataFrame({'y': [4, 5, 6] * 5, 'x': np.arange(15) + 1})

    data_source = ColumnDataSource(data0)

    button = Button()

    figure = Figure()
    figure.line('x', 'y', source=data_source)

    def updata_data():
        new_data = ColumnDataSource.from_df(data1)
        data_source.data.update(**new_data)

    button.on_click(updata_data)

    js_reset = CustomJS(args=dict(figure=figure), code="figure.reset.emit()")
    data_source.js_on_change('data', js_reset)

    curdoc().add_root(column(button, figure))

dashboard()

@Bryan What’s your current opinion on data ranges? Should they have some reset method to avoid having to use the workaround above?

Just as a note, the workaround I had mentioned is currently prevented by this known bug. This bug is unrelated to the original issue though, only the workaround.

I see. As a workaround for the workaround, you can:

  1. Remove the args from CustomJS
  2. Add id='my_figure' (or any other string) to Figure
  3. Change the code of the CustomJS to Bokeh.documents[0].get_model_by_id('my_figure').reset.emit()
1 Like

@p-himik I think it’s worth discussing. Different various kinds of “resets” are a relatively common topic.

1 Like