Bokeh: how to update/redraw the plot data after slider.on_change (with AJAX)?

I’m creating a heatmap + colormap with Bokeh + Flask. I would like to use Flask routes (instead of running a second Bokeh server involving Tornado…).

The following code works, but the slider1.on_change is now connected to a dummy CustomJS callback.

How to make that the slider changes trigger a re-sending of new data to the plot widget, with “AJAX”-like communication (here with a POST request /data/)?

import numpy as np
from flask import Flask, jsonify, request
from jinja2 import Template
from bokeh.models import ColorBar, LinearColorMapper
import bokeh.plotting, bokeh.embed, bokeh.models.callbacks, bokeh.models, bokeh.layouts
from bokeh.resources import INLINE

app = Flask(__name__)

template = Template('''<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Streaming Example</title>{{ js_resources }}{{ css_resources }}</head><body>{{ plot_div }}{{ plot_script }}</body></html>''')

def new_data(mu, sigma):
    return mu + np.random.randn(100, 100) * sigma

@app.route("/data", methods=['POST'])
def send_data():
    params = request.json
    Z = new_data(params["mu"], params["sigma"])
    return jsonify(z=[Z])

@app.route("/")
def simple():
    Z = new_data(mu=0, sigma=1)
    color_mapper = LinearColorMapper(palette="Viridis256", low=0, high=5)
    plot = bokeh.plotting.figure(x_range=(0, 1), y_range=(0, 1), height=200)
    plot.image(image=[Z], color_mapper=color_mapper, dh=1.0, dw=1.0, x=0, y=0)
    color_bar = ColorBar(color_mapper=color_mapper)
    plot.add_layout(color_bar, "right")
    callback = bokeh.models.callbacks.CustomJS(args=dict(), code="")
    slider1 = bokeh.models.Slider(start=-5, end=5, value=0, step=1, title="Mu")
    slider1.js_on_change('value', callback)
    slider2 = bokeh.models.Slider(start=1, end=20, value=10, step=1, title="Sigma")
    slider2.js_on_change('value', callback)
    layout = bokeh.layouts.column(plot, slider1, slider2)
    script, div = bokeh.embed.components(layout, INLINE)
    html = template.render(plot_script=script, plot_div=div, js_resources=INLINE.render_js(), css_resources=INLINE.render_css())
    return html

app.run(debug=True)

TL;DR: Is there a way to ask BokehJS to replace the heatmap’s content with data from an AJAX call?

JS Pseudo code which gets executed after a slider has moved:

fetch("/data").then(r => r.json()).then(data => {
    LinearColorMapper.update_content({ new_data: data });
});

?

Have you looked at AjaxDataSource ? You can set the polling interval to None and call its get_data method manually from a CustomJS callback to fetch new data from some endpoint. You shouldn’t need to update the color mapper unless there is something about the color mapping that you wanted to change (e.g. different palette).

1 Like

Thanks @Bryan! Would you have a small code example illustrating this use case? (maybe based on my example code, with a slider to change mu and sigma)

@stamljf I don’t have any thing at hand. Usually the best approach is for folks to make a stab at something first, and then if they get stuck, share the code they tried.

Thanks @Bryan. This nearly works, except:

  • it does a new /data POST request on each slider move, but it does not update the plot accordingly, how to do this? probably a change in the CustomJS code below?

  • how to get the new slider1, slider2 values in the route for /data? (here I did a dummy incrementation of a, b)

from flask import Flask, jsonify
from jinja2 import Template
import numpy as np
from bokeh.plotting import figure
from bokeh.models import AjaxDataSource, Slider
from bokeh.models.callbacks import CustomJS
from bokeh.embed import components
from bokeh.layouts import column
from bokeh.resources import INLINE
app = Flask(__name__)
a, b = 1, 0
@app.route("/data", methods=['POST'])
def get_x():
    global a, b
    a += 1      # in real use case, I would like to get the value of the "slider1"
    b += 1      #                                                    ... "slider2"
    x = np.arange(100)
    y = a * np.sin(x) + b
    return jsonify(x=x.tolist(), y=y.tolist())
template = Template('''<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Streaming Example</title> {{ js_resources }} {{ css_resources }} </head> <body> {{ plot_div }} {{ plot_script }} </body> </html> ''') 
@app.route("/")
def simple():
    source = AjaxDataSource(data_url="http://127.0.0.1:5000/data", polling_interval=None, mode='replace')
    fig = figure(title="Streaming Example")
    fig.line( 'x', 'y', source=source)
    callback = CustomJS(args=dict(source=source), code="""
        source.get_data();     // this triggers a new POST /data request... but it does not update the plot!
        """)
    slider1 = Slider(start=1, end=5, value=1, step=1, title="a")
    slider1.js_on_change('value', callback)
    slider2 = Slider(start=0, end=20, value=0, step=1, title="b")
    slider2.js_on_change('value', callback)
    layout = column(fig, slider1, slider2)
    script, div = components(layout, INLINE)
    html = template.render(plot_script=script, plot_div=div, js_resources=INLINE.render_js(), css_resources=INLINE.render_css())
    return html
app.run(debug=True)

TL;DR @Bryan

callback = CustomJS(args=dict(source=source), code="""
        source.get_data();     
        """)

triggers a new POST /data request… but it does not update the plot. How to ask for the plot to be updated?

I would have expected that to update the plot, the usua usage with a timeout interval just goes through that same path, and does update. I will have to investigate when I can get a chance.

Thanks @Bryan! Do you see a workaround/hack that I could use in the meantime?

(I’m in the start of the design of a big dashboard, so a temporary solution would be helpful to start)

I’ll have to actually investigate to be able to say anything concrete.

Ok @Bryan, thanks a lot in advance.
Maybe I can investigate too - feel free to tell me which files to look at, which lines approximatively, and I’ll report it here.

@stamljf Certainly, but do be aware that all of the actual work is done in JavaScript. You can see that when there is a polling interval, the Ajax data source just uses it to set up a setInterval callback for get_data:

Edit: and glancing at the base class code, I think the issue is that you are not passing a mode argument to get_data. The other arguments have defaults, but not mode, and some value is required to hit the codepaths that update the actual data internally. You can pass source.mode just like the snippet above does.

Thanks @Bryan! You’re right:

source.get_data(source.mode);

solved it :slight_smile:

Last thing: how would you make that @app.route("/data", methods=['POST']) def get_x(): … gets the right value of a and b, from the slider’s value? Ready-to-run example code, see line 15.

Pass both sliders in the args parameter to the CustomJS callback, then the callback can examine the slider’s current values (to update the URL or whatever) before calling get_data.