Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Control Bokeh plot state with HTTP Request

Tags:

python

bokeh

I'm creating a bokeh application, and would like to make the state (widgets, which affect the figure) "shareable". My first thought was to use a query string in the URL. However, I'm not sure if the actual HTTP request is available to the application.

An example:

# main.py
from bokeh.models.widgets import Select
from bokeh.plotting import Figure
from bokeh.io import curdoc, vform

p = Figure()
selector = Select(title="Select", options=["1", "2", "3"], value="2")

p.line([1, selector.value], [1, selector.value])

curdoc().add_root(vform(p, selector))

which is served with bokeh serve main.py and accessed at http://localhost:5006/main.

If I navigate to http://localhost:5006/main?select=3, is there a way for the application to know that the original request included select=3 and have that reflected in the figure going up to 3 instead of the default 2? Or am I approaching this entirely the wrong way, and missing a better solution?

This question and answer is related, but I think out of date now that bokeh server is built on tornado.

like image 367
TomAugspurger Avatar asked Apr 30 '26 13:04

TomAugspurger


1 Answers

Eventually I was able to get this working. I'm not sure I would really recommend anyone use this code, but in case it's useful....

I wrapped the Bokeh application in a Flask app. The basic idea is to parse the query parameters in the flask view, and walk the Bokeh document, updating the objects as directed by the query parameter.

I updated main.py to include state, a list of widgets controllable by the query parameter

# file: main.py
# ...
state = [selector]
# ...

We import that in app.py, the Flask (or Django/whatever) app.

# file: app.py
import json
from urllib.parse import urlencode

from flask import Flask, request, render_template

from bokeh.client import pull_session
from bokeh.embed import autoload_server
app = Flask(__name__)


@app.route('/')
def index():
    session = pull_session(app_path='/main')
    doc = session.document
    update_document(doc, request)  # this is the important part
    script = autoload_server(model=None,
                             app_path='/main', session_id=session.id)
    return render_template('index.html', script=script, session_id=session.id)


def update_document(doc, request):
    '''
    Update a Bokeh document according to the query string in ``request``

    Parameters
    ----------
    doc: Bokeh.Document
    request: flask.request

    Returns
    -------
    None: (Modifies doc inplace)
    '''
    for k, v in request.args.items():
        # titles must be unique
        model = doc.select_one(dict(title=k))
        v = parse_query_value(v)
        model.update(value=v)


def parse_query_value(v):
    '''
    Parse a single parameter's value from the query string

    Parameters
    ----------
    v : str

    Returns
    -------
    parsed : object
    '''
    if v.startswith('['):
        return json.loads(v.replace("'", '"'))
    return v


@app.route('/state/<session_id>')
def get_state(session_id):
    '''
    Get the current widget state.

    Parameters
    ----------
    session_id : int
        id of the connection to the bokeh server from ``session.id``.
        this should be unique per tab.
    '''
    session = pull_session(app_path='/main',
                           session_id=session_id)
    doc = session.document
    params = {}
    for key in state:
        params[key] = doc.select_one(dict(title=key)).value
    return urlencode(params)


if __name__ == '__main__':
    app.run()

And index.html

<html>
  <head>
    <meta charset="UTF-8">
    <title>Title</title>

  </head>
  <body>
    <h1 class='title'>Title</h1>

    <div class="plot">{{ script|safe }}</div>

  </body>

</html>
like image 110
TomAugspurger Avatar answered May 02 '26 03:05

TomAugspurger



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!