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 DataRange1D
s. 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:
- Remove the
args
fromCustomJS
- Add
id='my_figure'
(or any other string) toFigure
- Change the
code
of theCustomJS
toBokeh.documents[0].get_model_by_id('my_figure').reset.emit()
@p-himik I think it’s worth discussing. Different various kinds of “resets” are a relatively common topic.