Flask + Bokeh AjaxDataSource

Борьба с колбой + Bokeh AjaxDataSource:

У меня есть функция, которая возвращает данные json:

@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])

Я могу использовать его с Bokeh AjaxDataSource без проблем для создания графика потоковой передачи:

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

Однако, когда я пытаюсь встроить его в страницу флэшки, AjaxDataSource не запрашивает сервер. Сюжет не отображается без ошибок. Обратите внимание, что если я использую статический график вместо AjaxDataSource, он отлично подходит. Вот соответствующий код:

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) 

Если у кого-то есть мысли, я был бы благодарен.

Брайан

Ответ 1

Во-первых, как нежное предложение, пожалуйста, всегда отправляйте полные примеры исполняемых кодов. Потребовалось несколько минут, чтобы воспроизвести все недостающие необходимые импорты, за то, что потребовалось всего лишь секунд, чтобы диагностировать один раз, когда был запущен скрипт.


ОБНОВЛЕНИЕ: Поскольку Bokeh 0.12.15 обходной путь, описанный ниже, не требуется. AjaxDataSource должен AjaxDataSource без жалобы на пустой CDS без пустых столбцов, созданных спереди.


Недавно некоторые из кодовых путей BokehJS были сделаны более "строгими", что хорошо почти в каждом случае, но, похоже, это оставило плохое взаимодействие с AjaxDataSource который не был замечен. FWIW, когда я запускаю пример, я вижу ошибку в консоли JS браузера:

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

И это ключ к рабочему окружению, который заключается только в том, чтобы источник данных имел (пустые) столбцы для x и y:

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

Ниже приведен полный рабочий сценарий. Я бы попросил вас, пожалуйста, решить проблему с трекером Bokeh с этой информацией, чтобы эта ошибка могла быть приоритетной и исправлена


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
from bokeh.util.string import encode_utf8


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 encode_utf8(html)

app.run(debug=True)

Ответ 2

Как и OP, я также хотел использовать AJAX с Bokeh и Flask. Однако вместо непрерывной потоковой передачи данных с сервера с помощью AjaxDataSource я хотел получить новые данные с сервера только тогда, когда пользователь взаимодействует со входами на веб-странице. Чтобы добиться этого, я использовал в качестве основы ответ bigreddot, изменил AjaxDataSource на ColumnDataSource и добавил вызов JQuery AJAX внутри CustomJS (следующий пример был создан с Python 3.6.4, Flask 1.0.2 и 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)