12

Struggling with Flask + Bokeh AjaxDataSource:

I have a function that returns json data:

@app.route("/data", methods=['POST'])
def get_x():
    global x, y
    x = x + 0.1
    y = math.sin(x)
    return flask.jsonify(x=[x], y=[y])

I can use use it with a Bokeh AjaxDataSource no problem to create a streaming plot:

source = AjaxDataSource(data_url="http://localhost:5000/data", polling_interval=1000, mode='append')
p = figure()
p.line('x', 'y', source=source)                                                                       
show(p)

However, when I try to embed this in a flask page, the AjaxDataSource does not query the server. The plot fails to render with no errors. Note that if I use a static plot instead of an AjaxDataSource, it plots fine. Here's the relevant code:

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():
    streaming=True
    source = AjaxDataSource(data_url="http://localhost:5000/data", 
                            polling_interval=1000, mode='append')

    fig = figure(title="Streaming Example")
    fig.line( 'x', 'y', source=source)

    js_resources = INLINE.render_js()
    css_resources = INLINE.render_css()

    script, div = components(fig, INLINE)

    html = template.render(
        plot_script=script,
        plot_div=div,
        js_resources=js_resources,
        css_resources=css_resources
    )

    return encode_utf8(html) 

If anyone has any thoughts, I'd be thankful.

Brian

  • I would add that there is an official example in the bokeh repo: https://github.com/bokeh/bokeh/blob/1.2.0/examples/howto/ajax_source.py. – ostrokach Jul 12 '19 at 20:47

2 Answers2

13

First, as a gentle suggestion, please always post complete runnable code examples. It took a few minutes to reproduce all the missing necessary imports, for something that only took seconds to diagnose once there was a runnable script.


UPDATE: Since Bokeh 0.12.15 the workaround described below should not be required. AjaxDataSource should stream without complaint into an empty CDS with no empty columns created up front.


Recently some of the BokehJS code paths were made more "strict" which is good in almost every instance, but it appears this left a bad interaction with AjaxDataSource that was not noticed. FWIW when I run example I do see an error in the browser JS console:

Error: attempted to retrieve property array for nonexistent field 'x'

And this is the key to the workround, which is just to make sure the data source does have (empty) columns for x and y:

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

There is a complete working script below. I'd ask that you please make an issue on the Bokeh issue tracker with this information so that this bug can be prioritized and fixed


from flask import Flask, jsonify
from jinja2 import Template
import math

from bokeh.plotting import figure
from bokeh.models import AjaxDataSource
from bokeh.embed import components
from bokeh.resources import INLINE


app = Flask(__name__)

x, y = 0, 0

@app.route("/data", methods=['POST'])
def get_x():
    global x, y
    x = x + 0.1
    y = math.sin(x)
    return jsonify(x=[x], y=[y])

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():
    streaming=True
    source = AjaxDataSource(data_url="http://localhost:5000/data",
                            polling_interval=1000, mode='append')

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

    fig = figure(title="Streaming Example")
    fig.line( 'x', 'y', source=source)

    js_resources = INLINE.render_js()
    css_resources = INLINE.render_css()

    script, div = components(fig, INLINE)

    html = template.render(
        plot_script=script,
        plot_div=div,
        js_resources=js_resources,
        css_resources=css_resources
    )

    return html

app.run(debug=True)
bigreddot
  • 33,642
  • 5
  • 69
  • 122
  • Thanks so much. This worked perfectly. I'll post an issue as you suggested. Brian. – flailingsquirrel May 08 '16 at 17:07
  • I want to use the render_template and have the html file using jinja2 template. My html file is like this: {% extends "template.html" %} {% block content %}
    {{ script | safe}} {{ div | safe}}
    {% endblock %}. When I replace the return encode(html) with return render_template('testajax.html', script = script, div = div), I gt this error: WindowsError: [Error 32] The process cannot access the file because it is being used by another process. Is there anyway to fix this issue?
    – Hamid K May 13 '16 at 15:50
  • That doesn't seem like a Bokeh issue. Perhaps you have the file you are wanting to write to open in another process, maybe an editor? – bigreddot May 13 '16 at 16:04
8

Like the OP, I also wanted to use AJAX with Bokeh and Flask. However, instead of continuously streaming the data from the server with AjaxDataSource I wanted to get the new data from the server only when user interacts with the inputs on the web page. To accomplish this, I used bigreddot's answer as a basis, changed AjaxDataSource to ColumnDataSource and added a jQuery AJAX call inside CustomJS (the following example has been created with Python 3.6.4, Flask 1.0.2 and Bokeh 0.13.0):

import json

from flask import Flask, jsonify, request
from jinja2 import Template
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CustomJS, Select
from bokeh.embed import components
from bokeh.resources import INLINE
from bokeh.layouts import column
from bokeh.util.string import encode_utf8

app = Flask(__name__)

N_DATAPOINTS = 20
DEFAULT_VARIABLE = 'bar'
MY_DATABASE = {
    'foo': [i**1 for i in range(N_DATAPOINTS)],
    'bar': [i**2 for i in range(N_DATAPOINTS)],
    'baz': [i**3 for i in range(N_DATAPOINTS)]}


@app.route("/get_new_data", methods=['POST'])
def get_new_data():
    app.logger.info(
        "Browser sent the following via AJAX: %s", json.dumps(request.form))
    variable_to_return = request.form['please_return_data_of_this_variable']
    return jsonify({variable_to_return: MY_DATABASE[variable_to_return]})


SIMPLE_HTML_TEMPLATE = Template('''
<!DOCTYPE html>
<html>
    <head>
        <script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
        {{ js_resources }}
        {{ css_resources }}
    </head>
    <body>
    {{ plot_div }}
    {{ plot_script }}
    </body>
</html>
''')


@app.route("/")
def simple():
    x = range(N_DATAPOINTS)
    y = MY_DATABASE[DEFAULT_VARIABLE]

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

    plot = figure(title="Flask + JQuery AJAX in Bokeh CustomJS")
    plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
    callback = CustomJS(args=dict(source=source), code="""
    var selected_value = cb_obj.value;
    var plot_data = source.data;

    jQuery.ajax({
        type: 'POST',
        url: '/get_new_data',
        data: {"please_return_data_of_this_variable": selected_value},
        dataType: 'json',
        success: function (json_from_server) {
            // alert(JSON.stringify(json_from_server));
            plot_data.y = json_from_server[selected_value];
            source.change.emit();
        },
        error: function() {
            alert("Oh no, something went wrong. Search for an error " +
                  "message in Flask log and browser developer tools.");
        }
    });
    """)

    select = Select(title="Select variable to visualize",
                    value=DEFAULT_VARIABLE,
                    options=list(MY_DATABASE.keys()),
                    callback=callback)

    layout = column(select, plot)
    script, div = components(layout)
    html = SIMPLE_HTML_TEMPLATE.render(
        plot_script=script,
        plot_div=div,
        js_resources=INLINE.render_js(),
        css_resources=INLINE.render_css())

    return encode_utf8(html)

app.run(debug=True, host="127.0.0.1", port=5002)
tuomastik
  • 4,559
  • 5
  • 36
  • 48
  • Thanks a lot for your code. Do you know by chance if it is possible to replace built-in bokeh's Select by regular HTML select functionality? How to implement ajax call in that case? Usually ajax call is maid from .js file, not inside of flask app. I am trying to build an interactive flask app without bokeh sever avoiding the usage of built-in widgets. Any thought? Thanks – chemist Feb 08 '17 at 14:08
  • My pleasure! Unfortunately I don't know how to replace the built-in Bokeh select box. I would like to know how to do that as well, because I would like to use a searchable select box like Select2 since one of the apps that I've developed contains a select box with hundreds of items which makes it painful to scroll through the long list to find the item that you're looking for. If you will figure this out, please let me know :) Good luck! – tuomastik Feb 08 '17 at 16:10
  • Thanks for your reply. I will definitely let you know if I find the solution. At this point I really doubt that it is possible. Good luck to you too! – chemist Feb 13 '17 at 18:37
  • 1
    Thank you, I think this is exactly what I've been looking for – jjepsuomi Jan 20 '22 at 21:28