How can I correctly obtain the updated x_range after dragging in Jupyter?

In Jupyter using Bokeh, I need the x_range of p . However, after dragging, the x_range does not update and remains at its initial value. Why is this happening? Below is my code and a screenshot. I used a simple example to demonstrate the issue. After dragging, the print in the subsequent cell still shows the initial values.

import numpy as np

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, RangeTool
from bokeh.plotting import figure, show

x = np.linspace(0, 10, 500)
y = np.sin(x)

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

p = figure(height=300, width=800, tools="xpan", toolbar_location=None,
           x_axis_type="datetime", x_axis_location="above",
           background_fill_color="#efefef", x_range=(x[15], x[25]))

p.line('x', 'y', source=source)
p.yaxis.axis_label = 'Price'

select = figure(title="Drag the middle and edges of the selection box to change the range above",
                height=130, width=800, y_range=p.y_range,
                x_axis_type="datetime", y_axis_type=None,
                tools="", toolbar_location=None, background_fill_color="#efefef")

range_tool = RangeTool(x_range=p.x_range)
range_tool.overlay.fill_color = "navy"
range_tool.overlay.fill_alpha = 0.2

select.line('x', 'y', source=source)
select.ygrid.grid_line_color = None
select.add_tools(range_tool)

show(column(p, select))
print("Initial x_range:", range_tool.x_range.start, p.x_range.end)

In your current code, the BokehJS content is embedded in the output cell, and there is no connection between the BokehJS side of things and the Python kernel.

If you want full bi-directional updates between Javascript and the Python kernel, you would need to embed a Bokeh server app (making that synchronization happen automatically is the purpose of the Bokeh server). See e.g. examples/server/api/notebook_embed.ipynb

@haojiahuoX, I don’t exactly know what you want to do with the x_range of p, but as Bryan said, you have to build a server to access it in real time.

You could rewrite your code as follows:

import numpy as np

from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, RangeTool, Paragraph
from bokeh.plotting import figure

x = np.linspace(0, 10, 500)
y = np.sin(x)

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

p = figure(height=300, width=800, tools="xpan", toolbar_location=None,
           x_axis_type="datetime", x_axis_location="above",
           background_fill_color="#efefef", x_range=(x[15], x[25]))

p.line('x', 'y', source=source)
p.yaxis.axis_label = 'Price'

select = figure(title="Drag the middle and edges of the selection box to change the range above",
                height=130, width=800, y_range=p.y_range,
                x_axis_type="datetime", y_axis_type=None,
                tools="", toolbar_location=None, background_fill_color="#efefef")

range_tool = RangeTool(x_range=p.x_range)
range_tool.overlay.fill_color = "navy"
range_tool.overlay.fill_alpha = 0.2

select.line('x', 'y', source=source)
select.ygrid.grid_line_color = None
select.add_tools(range_tool)

start_par = Paragraph(text=f"range_tool.x_range.start: {range_tool.x_range.start}, p.x_range.start: {p.x_range.start}")
end_par = Paragraph(text=f"range_tool.x_range.end: {range_tool.x_range.end}, p.x_range.end: {p.x_range.end}")

def callback_start(attr, old, new):
    start_par.text = f"range_tool.x_range.start: {range_tool.x_range.start}, p.x_range.start: {p.x_range.start}"

def callback_end(attr, old, new):
    end_par.text = f"range_tool.x_range.end: {range_tool.x_range.end}, p.x_range.end: {p.x_range.end}"

range_tool.x_range.on_change("start", callback_start)
range_tool.x_range.on_change("end", callback_end)

layout = column(p, select, start_par, end_par)

curdoc().add_root(layout)

and run it in a Python powershell with bokeh serve --show file_name.py.

To run it in JupyterLab, modify the bokeh.io import to:
from bokeh.io import curdoc, show, output_notebook
After the imports, add output_notebook() in a free line,
at the end, replace curdoc().add_root(layout) by:

def my_app(doc):
    doc.add_root(layout)
show(my_app)

In the callback functions, you then have access to the real time value of start and end of the x-axis of p.

Just a note: in the notebook you really want to have new Bokeh objects created for every (re-)evaluation of the cell with show. Each new invocations results in a new session, and sessions are not designed to share objects. Typically all the “app” code is encapsulated entirely in a function like my_app, not just the add_root part. This is how things are demonstrated in the example I linked.

Alternatively, a small class can be defined for the app, which may be preferable for code organization. There was an example of that discussed here:

Thank you for your response. In order to avoid server calls, I initially used ipywidgets to interact with the Python kernel, and I needed to obtain the x_range in the process to invoke a computation module and display another image. However, it seems that the approach of using ipywidgets may not be correct. I will try building a server to address this.

Thank you for your response. This example has been very helpful for me. I will try to encapsulate an app to address my issue.

@haojiahuoX There is some integration with Bokeh and ipywidgets but I am not really familiar with it (or its current state) cc @mateusz @Philipp_Rudiger

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.