Communication between a JS page and a Bokeh plot

Hello,
I am trying to implement a communication between a JS page and a bokeh plot, interactively.

I would like the JS page :

  1. to be notified when I click on a curve of my plot
  2. to be able to change online the data source, so that plot can change dinamically

Do you have any suggestion?

As to 1) I tried to use js_on_change callbacks, but from that JS code that I wrote, I cannot access variables and functions in my JS page
As to 2) the only possibility I see so far is to reload entirely the bokeh plot, and pass to the server some flags to selected the data to be plotted, this however will regenerate the entire plot and not change it…

Do you have example to suggest as a staring point?

best regards,
Luigi

1 Like

Some updates from my side.

Related to 2), I have understood that flask can communicate with a boken server both ways by sharing the session.document object. I am trying to implement a simple example, however, I am stuck with some unexpected behavior. I have opened a separate thread Read and modify a bokeh slider value from flask

As I mentioned, I also was able to get arguments from flask form URL of the bokeh server, using

# request.arguments is a dict that maps argument names to lists of strings,
# e.g, the query string ?N=10 will result in {'N': [b'10']}

args = curdoc().session_context.request.arguments

try:
  N = int(args.get('N')[0])
except:
  N = 200

However this works only once the plot is generated the first time and does not allow to be interactive.

Do you see other best practices related to 2) that I am not aware of?

Related to 1) my understanding is that js_on_event() can work only if the injected js code lives in the same JS page which include the plots, which may not be the case. In my setting, I am interested in having the plot in a React page.

best,
Luigi

Hello,
I have been exploring possible solutions for 1).
So far I see two possibilities, and I would like to ask you which is the suggested one in your option.

a) currently my plot is served to me by flask which talks with a bokeh server.

return render_template("embed.html", script=script, template="Flask")

My understanding is that one way of proceeding would be to call the url of the API of flask which returns the plot from a iframe, This however make impossible for js_on_event to access the JS variables of my JS page. I use two options: I ask flask for variables’ changes when I need, or I implement a websocket on my JS page to be notified.

b) I embed the plot using bokehjs, with json_dumps, see Embedding Bokeh into a ReactJS app using BokehJS – David Vassallo's Blog
However my understanding is that in this case the control on the session is lost, since I do not have a bokeh server and this does not allow for instance to interact with the plot to remove or add lines, for instance. On the other side, I can image this allows js_on_change code to run on my JS page and thus be able to access functions and variables there, thus avoiding the need of a websocket on the JS page to be notified by changes. I am not sure about this last point.

Which way do you suggest?

Thanks for your help.

I am bit unsure if I have understood your question correctly but if you have a TapTool to register when a glyph is being hit then execute a js_on_event . In the CustomJS callback you can have references to other JS functions that you load. In the example below I load styles and JS functions where I can refer to a JS function from Bokeh CustomJS.

{% extends base %}

{% block preamble %}
  <link href="{{ root }}/static/css/styles.css" rel="stylesheet">
  <script type="text/javascript" src="{{ root }}/static/js/functions.js"></script>
{% endblock %}
{% block contents %}
{% endblock %}

Dear Jonas,
Thanks for your reply. Can I ask you how you integrate the Bokeh script in your HTML page?
Is your approach compatible with having the Bokeh plot provided by Flask managing a Bokeh server through a session?

Thanks,
Luigi

Hi Luigi,
Sorry about not showing any code example. In the example below I run Flask that embeds a server_session. I am using the sliders.py code with some changes. I make use of CustomJS in order to run JS code from Bokeh. In this example I attached a callback to a TapTool. The callback will update the text in a non-Bokeh document element when one taps/click the line in the plot.

tap_callback = CustomJS(
    args = {
        'src': source
    },
    code='''
        // Bokeh JS code
        console.log("tap callback");
        
        // if one needs to use the index of the source data for something
        // use .selected.indicies if circle/scatter glyph
        //const idx = src.selected.indices;
        const idx = src.selected.line_indices;
        
        // update a non-Bokeh document element
        const elm = document.getElementsByClassName("bk-plot-info")[0];
        elm.innerText = "You tapped the line in Bokeh plot";

        // ... or have the Bokeh JS in an imported JS function loaded in header
        bokeh_func(); 
  '''
)
tap_tool = TapTool()
tap_tool.callback = tap_callback
plot.add_tools(tap_tool)

One enters JS code in the code argument for CustomJS as text string. Either the whole code or reference to a JS function that has been loaded from external file defined in header.

I also make use of the name argument in the definition of the figure object (name=plot). This in order to reference and update the Bokeh object from a non-Bokeh JS function. This happens through the JS function updateBokehPlotTitle which is attached to the non-Bokeh input form. In this function one gets hold of the Bokeh plot (since we want to update the title) using BokehJS get_model_by_name:

function updateBokehPlotTitle(form) {
	console.log('Update Bokeh title');
	const bkplt = window.Bokeh.documents[0].get_model_by_name('plot');
	bkplt.title.text = form.title.value;
}

All the code is below where I have it in multiple files. Css styles and JS functions are within static/css and static/js respectively.
Bokeh sliders app started with

bokeh serve --allow-websocket-origin=localhost:8000 sliders.py

Hope it helps (and that I have understood your question correctly).

#app.py
from flask import Flask, render_template
from bokeh.embed import server_document, server_session
from bokeh.client import pull_session

BK_URL = 'http://localhost:5006/'
BK_ENDPOINT = 'sliders'

app = Flask(__name__)

session_id = None

@app.route('/')
def index():
    url=f'{BK_URL}{BK_ENDPOINT}'
    with pull_session(url=url) as session:
        bk_script = server_session(session_id=session.id, url=url)

    return render_template('index.html', bk_script=bk_script)

if __name__ == '__main__':
    app.run(port=8000, debug=True)

# sliders.py
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput
from bokeh.models import TapTool, CustomJS
from bokeh.plotting import figure

# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))


# Set up plot
plot = figure(
    height=400,
    width=400,
    title="my sine wave",
    tools="crosshair,pan,reset,save,wheel_zoom",
    x_range=[0, 4*np.pi],
    y_range=[-2.5, 2.5],
    name = 'plot'
    )

plot.line('x', 'y', source=source, line_width=4, line_alpha=0.6)

tap_callback = CustomJS(
    args = {
        'src': source
    },
    code='''
        // Bokeh JS code
        console.log("tap callback");
        
        // if one needs to use the index of the source data for something
        // use .selected.indicies if circle/scatter glyph
        //const idx = src.selected.indices;
        const idx = src.selected.line_indices;
        
        // update a non-Bokeh document element
        const elm = document.getElementsByClassName("bk-plot-info")[0];
        elm.innerText = "You tapped the line in Bokeh plot";

        // ... or have the Bokeh JS in an imported JS function loaded in header
        bokeh_func(); 
  '''
)
tap_tool = TapTool()
tap_tool.callback = tap_callback
plot.add_tools(tap_tool)

# Set up widgets
text = TextInput(title="title", value='my sine wave', name = 'title_text')
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)


# Set up callbacks
def update_title(attrname, old, new):
    plot.title.text = text.value

text.on_change('value', update_title)

def update_data(attrname, old, new):

    # Get the current slider values
    a = amplitude.value
    b = offset.value
    w = phase.value
    k = freq.value

    # Generate the new curve
    x = np.linspace(0, 4*np.pi, N)
    y = a*np.sin(k*x + w) + b

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

for w in [offset, amplitude, phase, freq]:
    w.on_change('value', update_data)


# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq, name = 'input_wdg')

curdoc().add_root(row(inputs, plot, width=800))

curdoc().title = "Sliders"

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet" type="text/css">
    <script type="text/javascript" src="{{ url_for('static', filename='js/functions.js') }}"></script>
  </head>
  <body>
    <div class="container">
      <div>
        <p class="bk-plot-info">None-Bokeh doc element</p>
        <form onsubmit="return false">
          <label for="name">Update title of Bokeh plot</label>
          <input type="text" id="name" name="title">
          <input type="button" value="Update" onclick="updateBokehPlotTitle(this.form)">
        </form>
      </div>
      <div>
        <h3>Bokeh session</h3>
        <p>Tap line glyph using TapTool to update non-Bokeh doc element</p>
        <div>
          {{ bk_script | safe }}
        </div>
      </div>
    </div>
  </body>
</html>
// functions.js
function bokeh_func() {
	console.log('Hello from imported function called through Bokeh CustomJS');
}

function updateBokehPlotTitle(form) {
	console.log('Update Bokeh title');
	const bkplt = window.Bokeh.documents[0].get_model_by_name('plot');
	bkplt.title.text = form.title.value;
}
/*styles.css*/
html {
  font-family: sans-serif;
  font-size: 62.5%; 
  -ms-text-size-adjust: 100%; 
  -webkit-text-size-adjust: 100%; 
}

body {
  font-size: 1.6em; 
  line-height: 1.5;
  font-weight: 300;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Oxygen, Cantarell, sans-serif;
  color: #444444;
  margin: 0px;
  background-color: white;
  padding-top: 0px;
}

.container {
  max-width: 800px;
  margin: auto;
}

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