2

I need to visualize embedded multiple graphs from streaming data on a web browser for a quick visualization. Alas my knowledge of Javascript is null and I thought to know a little of Bokeh before this test, but it looks like I don't.

I am very confused by the dozens of examples that you can find around but don't work with the last Bokeh library (0.12.6). Already gave a look to Streaming two line graphs using bokeh, but alas I need to use Flask.

I help to understand where the proper direction to solve the problem is, not to rewrite my code.

Up to now I found a solution but it is very CPU consuming (this is a working code, sorry for the length: I stripped it down already). Again, it refreshes the whole page instead of the graphs only. It is ok in Chromium 58 (Linux), very slow in Firefox 53 (Linux).

Change Bokeh to another library is an option. Flask is compulsory.

app.py:

from bokeh.models import (FactorRange, LinearAxis, Grid, Range1d)
from bokeh.models.glyphs import Line
from bokeh.plotting import figure
from bokeh.embed import components
from bokeh.models.sources import ColumnDataSource

from flask import Flask, render_template

import random

app = Flask(__name__)


class SingleLine():
    def __init__(self, color, elem_number):

        self.color = color
        self.elem_number = elem_number

        self.data = {"time": [], "value": []}
        for i in range(1, elem_number + 1):
            self.data['time'].append(i)
            self.data['value'].append(random.randint(1,100))

    def create_line_plot(self, plot, x_name, y_name):
        source = ColumnDataSource(self.data)
        glyph = Line(x=x_name, y=y_name, line_color=self.color)
        plot.add_glyph(source, glyph)

#----------------------------------------------------------------------------------

class CompleteGraph():

    def __init__(self, lines_list, x_name, y_name, title, height=300, width=1000):
        self.lines_list = lines_list
        self.x_name = x_name
        self.y_name = y_name
        self.title = title
        self.height = height
        self.width = width

    def create_graph(self):

        xdr = FactorRange(factors=self.lines_list[0].data[self.x_name])
        ydr = Range1d(start=0, end=max(self.lines_list[0].data[self.y_name]) * 1.5)

        plot = figure(title=self.title, x_range=xdr, y_range=ydr,
                      plot_width=self.width, plot_height=self.height,
                      h_symmetry=False, v_symmetry=False,
                      tools=[],
                      responsive=True)

        for l in self.lines_list:
            l.create_line_plot(plot, self.x_name, self.y_name)

        xaxis = LinearAxis()
        yaxis = LinearAxis()

        plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker))
        plot.add_layout(Grid(dimension=1, ticker=yaxis.ticker))

        return components(plot)


@app.route("/")
def chart():
    elem_number = 30

    line1 = SingleLine(color="#ff0000", elem_number=elem_number)
    line2 = SingleLine(color="#00ff00", elem_number=elem_number)
    line3 = SingleLine(color="#00ff00", elem_number=elem_number)

    # first graph
    lines_list = [line1, line2]
    lg1 = CompleteGraph(lines_list, "time", "value", "title graph 1")

    # second graph
    lines_list = [line1, line3]
    lg2 = CompleteGraph(lines_list, "time", "value", "title graph 2")

    script1, div1 = lg1.create_graph()
    script2, div2 = lg2.create_graph()

    return render_template("test_stackoverflow.html",
                           div1=div1, script1=script1,
                           div2=div2, script2=script2,
                           )


if __name__ == "__main__":
    app.run(port=5000, debug=True)

and the respective template:

test_stackoverflow.html

<html>
    <head>
        <style>
            #wrapper { display: flex; }
            #left { flex: 0 0 50%; }
            #right { flex: 1; }
            #wide { flex: 0 0 90% }
        </style>

        <title>Multiple realtime charts with Bokeh</title>
        <link href="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.6.min" rel="stylesheet">
        <link href="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.6.min.css" rel="stylesheet">

        <script src="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.6.min.js"></script>
        <script src="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.6.min.js"></script>

        <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

        <script>
            (function worker() {
                $.ajax({
                    url: '/',
                    success: function(data) {
                       location.reload();
                    },
                    complete: function() {
                       // Schedule the next request when complete
                       setTimeout(worker, 1000);
                    }
                });
            })();
        </script>
    </head>

    <body>
        <div id="wrapper">
            <div id="left">
                <h1>left graph</h1>
                {{ div1 | safe }}
                {{ script1 | safe }}
            </div>

            <div id="right">
                <h1>right graph</h1>
                {{ div2 | safe }}
                {{ script2 | safe }}
            </div>
        </div>
    </body>
</html>

Any help is welcome.

bigreddot
  • 33,642
  • 5
  • 69
  • 122
Alex Poca
  • 2,406
  • 4
  • 25
  • 47

1 Answers1

4

If somebody is interested in it, here is a working (very raw) example, based on bokeh /examples/howto/ajax_source.py

app.py:

import numpy as np
from datetime import timedelta
from functools import update_wrapper, wraps
from math import sin, cos
from random import random
from six import string_types

from bokeh.plotting import figure
from bokeh.models.sources import AjaxDataSource
from bokeh.embed import components


from flask import Flask, jsonify, make_response, request, current_app, render_template


#########################################################
# Flask server related
#
# The following code has no relation to bokeh and it's only
# purpose is to serve data to the AjaxDataSource instantiated
# previously. Flask just happens to be one of the python
# web frameworks that makes it's easy and concise to do so
#########################################################


def crossdomain(origin=None, methods=None, headers=None,
                max_age=21600, attach_to_all=True,
                automatic_options=True):
    """
    Decorator to set crossdomain configuration on a Flask view

    For more details about it refer to:

    http://flask.pocoo.org/snippets/56/
    """
    if methods is not None:
        methods = ', '.join(sorted(x.upper() for x in methods))

    if headers is not None and not isinstance(headers, string_types):
        headers = ', '.join(x.upper() for x in headers)

    if not isinstance(origin, string_types):
        origin = ', '.join(origin)

    if isinstance(max_age, timedelta):
        max_age = max_age.total_seconds()

    def get_methods():
        options_resp = current_app.make_default_options_response()
        return options_resp.headers['allow']

    def decorator(f):
        @wraps(f)
        def wrapped_function(*args, **kwargs):
            if automatic_options and request.method == 'OPTIONS':
                resp = current_app.make_default_options_response()
            else:
                resp = make_response(f(*args, **kwargs))
            if not attach_to_all and request.method != 'OPTIONS':
                return resp

            h = resp.headers

            h['Access-Control-Allow-Origin'] = origin
            h['Access-Control-Allow-Methods'] = get_methods()
            h['Access-Control-Max-Age'] = str(max_age)
            requested_headers = request.headers.get(
                'Access-Control-Request-Headers'
            )
            if headers is not None:
                h['Access-Control-Allow-Headers'] = headers
            elif requested_headers:
                h['Access-Control-Allow-Headers'] = requested_headers
            return resp
        f.provide_automatic_options = False
        return update_wrapper(wrapped_function, f)

    return decorator




app = Flask(__name__)

x = list(np.arange(0, 6, 0.1))
y1 = [sin(xx) + random() for xx in x]
y2 = [sin(xx) + random() for xx in x]

@app.route('/data', methods=['GET', 'OPTIONS', 'POST'])
@crossdomain(origin="*", methods=['GET', 'POST'], headers=None)
def hello_world():
    x.append(x[-1]+0.1)
    y1.append(sin(x[-1])+random())
    y2.append(cos(x[-1])+random())
    return jsonify(x=x[-500:],
                    y1=y1[-500:],
                    y2=y2[-500:],
                )



@app.route("/")
def main_graph():

    source = AjaxDataSource(data=dict(x=[], y1=[], y2=[], y3=[]),
                            data_url='http://127.0.0.1:5050/data',
                            polling_interval=1000)

    p = figure()
    p.line(x='x', y='y1', source=source)

    p.x_range.follow = "end"
    p.x_range.follow_interval = 10

    script1, div1 = components(p)

    # -------------------------------------
    p = figure()
    p.line(x='x', y='y2', source=source)

    p.x_range.follow = "end"
    p.x_range.follow_interval = 10

    script2, div2 = components(p)

    return render_template("test_stackoverflow.html",
                           div1=div1, script1=script1,
                           div2=div2, script2=script2,
                           )

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5050, debug=True)

test_stackoverflow.html:

<!DOCTYPE html>

<html>
    <head>
        <style>
            #wrapper { display: flex; }
            #left { flex: 0 0 50%; }
            #right { flex: 1; }
            #wide { flex: 0 0 90% }
        </style>

        <title>Multiple realtime charts with Bokeh</title>
        <link href="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.6.min" rel="stylesheet">
        <link href="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.6.min.css" rel="stylesheet">

        <script src="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.6.min.js"></script>
        <script src="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.6.min.js"></script>

        <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>


    </head>

    <body>
        <div id="wrapper">
            <div id="left">
                <h1>left graph</h1>
                {{ div1 | safe }}
                {{ script1 | safe }}
            </div>

            <div id="right">
                <h1>right graph</h1>
                {{ div2 | safe }}
                {{ script2 | safe }}
            </div>
        </div>
    </body>
</html>
bigreddot
  • 33,642
  • 5
  • 69
  • 122
Alex Poca
  • 2,406
  • 4
  • 25
  • 47
  • 1
    As an addon, instead of the (ugly) `crossdomain` function, you can use instead flask-cors: `flask_cors.CORS(app) ` will add crossdomain for all routes, origins and methods. That's all. – Alex Poca Mar 19 '18 at 09:04