Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update a bokeh plot using ajax

I have a similar question as this one. I have a website containing several elements, such as a table and a bokeh plot which I want to update once the user gives a certain input. While I can accomplish this for the table, I don't know how to update the bokeh plot.

In the example below, I use a minimal example where the user input determines the dimensions of a table. So an output looks like this:

enter image description here

The update of the table works great and as expected, however, I struggle to update the plot accordingly. I can initialize it easily using (entire code can be found at the end of this post):

@app.route('/')
def index():

    # just an initial figure
    p = figure(plot_width=150, plot_height=100)

    p.line([1, 2, 3], [1, 2, 3])
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)
    return render_template('index.html', div_bok=div_bok, script_bok=script_bok)

and can then fetch the user's choice by

@app.route('/_get_table')
def get_table():

    nrow = request.args.get('nrow', type=int)
    ncol = request.args.get('ncol', type=int)

    # the table we want to display
    df = pd.DataFrame(np.random.randint(0, 10, size=(nrow, ncol)))

    # the updated/new plot
    p = figure(plot_width=150, plot_height=100)

    p.line(list(range(nrow)), list(range(nrow)))
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)

    # how would I pass those now?

    return jsonify(my_table=json.loads(df.to_json(orient="split"))["data"],
                   columns=[{"title": str(col)} for col in json.loads(df.to_json(orient="split"))["columns"]],
                   div_bok=div_bok,
                   script_bok=script_bok)

The question is: How do I now feed the new components to index.html so that the plot is updated along with the table but all the other elements on the page are untouched?

There are now two answers that solve the issue. I still decided to use a bounty to find even better answers. Things that could be improved:

1) Right now, the plot is always drawn from scratch, ideally, only the data would be updated.

2) Is there a way to avoid the additional template (below it is called update_content.html)?

If no additional answer shows up, I am more than happy to provide the points to @Anthonydouc for his nice answer.

This is my entire code:

from flask import Flask, render_template, request, jsonify
import pandas as pd
import numpy as np
import json
from bokeh.plotting import figure, show, save
from bokeh.embed import components

# Initialize the Flask application
app = Flask(__name__)


@app.route('/')
def index():

    # just an initial figure
    p = figure(plot_width=150, plot_height=100)

    p.line([1, 2, 3], [1, 2, 3])
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)
    return render_template('index.html', div_bok=div_bok, script_bok=script_bok)


@app.route('/_get_table')
def get_table():

    nrow = request.args.get('nrow', type=int)
    ncol = request.args.get('ncol', type=int)

    # the table we want to display
    df = pd.DataFrame(np.random.randint(0, 10, size=(nrow, ncol)))

    # the updated/new plot
    p = figure(plot_width=150, plot_height=100)

    p.line(list(range(nrow)), list(range(nrow)))
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)

    # how would I pass those now?

    return jsonify(my_table=json.loads(df.to_json(orient="split"))["data"],
                   columns=[{"title": str(col)} for col in json.loads(df.to_json(orient="split"))["columns"]],
                   div_bok=div_bok,
                   script_bok=script_bok)



if __name__ == '__main__':

    app.run(debug=True, threaded=True)

and my index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
    <link href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css" rel="stylesheet">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.css" rel="stylesheet" type="text/css">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div class="container">
      <div class="header">
          <h3 class="text-muted">some stuff</h3>
        </div>

        <hr class="mb-4">
     <div class="row">
      <div class="col-md-8">
      <form id="input_data_form" class="needs-validation" novalidate>
        <div class="row">
          <div class="col-md-6 mb-3">
            <label for="nrow">rows</label>
            <input type="number" class="form-control" id="nrow" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
          <div class="col-md-6 mb-3">
            <label for="ncol">columns</label>
            <input type="number" class="form-control" id="ncol" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
        </div>
        <div class="form-row text-center">
          <div class="col-12">
              <button id="calculate" type="submit" class="btn btn-primary">Calculate!</button>
          </div>
       </div>
      </form>
      </div>
        <div class="col-md-4">

          <div class="header">
            <h5 class="text-muted">Plot results</h5>
          </div>
          {{div_bok|safe}}
          {{script_bok|safe}}
        </div>
     </div>

      <hr class="mb-4">

      <table id="a_nice_table" class="table table-striped"></table>

    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js" type="text/javascript"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.js"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.js"></script>
    <script type="text/javascript">
      $(document).ready(function() {
        var table = null;
        $('#input_data_form').submit(function (event) {
            event.preventDefault();
            if ($('#input_data_form')[0].checkValidity() === false) {
                event.stopPropagation();
                if (table !== null) {
                    table.destroy();
                    table = null;
                    $("#a_nice_table").empty();
                  }
            } else {
                $.getJSON('/_get_table', {
                    nrow: $("#nrow").val(),
                    ncol: $("#ncol").val()

                  }, function(data) {

                  //document.body.append($(data.script_bok)[0]);

                  //$("#bokeh_plot").html(data.div_bok);

                  if (table !== null) {
                    table.destroy();
                    table = null;
                    $("#a_nice_table").empty();
                  }
                  table = $("#a_nice_table").DataTable({
                    data: data.my_table,
                    columns: data.columns

                  });
                });
            return false;
            }
            $('#input_data_form').addClass('was-validated');
        });

      });
    </script>
  </body>
like image 377
Cleb Avatar asked May 11 '18 17:05

Cleb


2 Answers

I have deleted the table from your example, and just focused on updating the bokeh plot. Important to note in this example the plot is being re-created each time you press calculate.

There are now three files - server code (app.py), base template (index.html) and a template to render only bokeh plot (update_content.html). Both the templates need to be in the templates folder as usual.

The '_get_table' endpoint now returns rendered html containing the bokeh plot. Clicking the calculate button will trigger a callback, which in turn submits a post request to this endpoint. app.py:

from flask import Flask, render_template, request, jsonify
import pandas as pd
import numpy as np
import json
from bokeh.plotting import figure, show, save
from bokeh.embed import components

# Initialize the Flask application
app = Flask(__name__)


@app.route('/')
def index():

    # just an initial figure
    p = figure(plot_width=150, plot_height=100)

    p.line([1, 2, 3], [1, 2, 3])
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)
    return render_template('index.html', div_bok=div_bok, script_bok=script_bok)


@app.route('/_get_table', methods=['GET','POST'])
def get_table():

    # extract nrow, ncol via ajax post - contained in request.form
    nrow = request.form.get('nrow', type=int)
    ncol = request.form.get('ncol', type=int)

    # the updated/new plot
    p = figure(plot_width=150, plot_height=100)

    p.line(list(range(nrow)), list(range(nrow)))

    script_bok, div_bok = components(p)

    #return rendered html to the browser

    return render_template('update_content.html', div_bok=div_bok, script_bok=script_bok)



if __name__ == '__main__':

    app.run(debug=True, threaded=True)

Index.html :

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
    <link href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css" rel="stylesheet">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.css" rel="stylesheet" type="text/css">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div class="container">
      <div class="header">
          <h3 class="text-muted">some stuff</h3>
        </div>

        <hr class="mb-4">
     <div class="row">
      <div class="col-md-8">
      <form id="input_data_form" class="needs-validation" novalidate>
        <div class="row">
          <div class="col-md-6 mb-3">
            <label for="nrow">rows</label>
            <input type="number" class="form-control" id="nrow" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
          <div class="col-md-6 mb-3">
            <label for="ncol">columns</label>
            <input type="number" class="form-control" id="ncol" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
        </div>
        <div class="form-row text-center">
          <div class="col-12">
              <button id="calculate" type="submit" class="btn btn-primary">Calculate!</button>
          </div>
       </div>
      </form>
      </div>
        <div class="col-md-4">
          <div class="header">
            <h5 class="text-muted">Plot results</h5>
          </div>
          <div id="plot-content">
            {{div_bok|safe}}
            {{script_bok|safe}}
          </div>
        </div>
     </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js" type="text/javascript"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.js"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.js"></script>
    <script type="text/javascript">
      $(document).ready(function(){
        $('#calculate').on('click', function(e){
          // prevent page being reset, we are going to update only
          // one part of the page.
          e.preventDefault()
          $.ajax({
            url:'./_get_table',
            type:'post',
            data:{'nrow':$("#nrow").val(),
                  'ncol':$("#ncol").val()},
            success : function(data){
              // server returns rendered "update_content.html"
              // which is just pure html, use this to replace the existing
              // html within the "plot content" div
              $('#plot-content').html(data)
            }
          })
        });
      });
    </script>
  </body>

update_content.html

{{div_bok|safe}}
{{script_bok|safe}}
like image 71
Anthonydouc Avatar answered Nov 15 '22 23:11

Anthonydouc


@Anthonydouc's excellent answer got me on the right track, so all the credit should go to him; this is just an extension on how one can also update the table at the same time.

The key is to create an additional file, here update_content.html that will contain the required HTML for the plot which should be updated everytime the user provides some input. It contains only:

{{div_bok|safe}}
{{script_bok|safe}}

Given this, one can then use render_template to produce the HTML string that represents the figure and passes it also via jsonify. The updated flask script therefore looks as follows:

from flask import Flask, render_template, request, jsonify
import pandas as pd
import numpy as np
import json
from bokeh.plotting import figure, show, save
from bokeh.embed import components

# Initialize the Flask application
app = Flask(__name__)


@app.route('/')
def index():

    return render_template('index.html')


@app.route('/_get_table')
def get_table():

    nrow = request.args.get('nrow', type=int)
    ncol = request.args.get('ncol', type=int)

    # the table we want to display
    df = pd.DataFrame(np.random.randint(0, 10, size=(nrow, ncol)))

    # the updated/new plot
    p = figure(plot_width=150, plot_height=100)

    p.line(list(range(nrow + 1)), list(range(nrow + 1)))
    # save(p, 'testing.html')

    script_bok, div_bok = components(p)

    # pass the div and script to render_template    
    return jsonify(my_table=json.loads(df.to_json(orient="split"))["data"],
                   columns=[{"title": str(col)} for col in json.loads(df.to_json(orient="split"))["columns"]],
                   html_plot=render_template('update_content.html', div_bok=div_bok, script_bok=script_bok))


if __name__ == '__main__':

    app.run(debug=True, threaded=True)

and the index.html file looks like this (make sure to use the correct bokeh version, here it is 0.12.15):

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
    <link href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css" rel="stylesheet">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.css" rel="stylesheet" type="text/css">
    <link href="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div class="container">
      <div class="header">
          <h3 class="text-muted">some stuff</h3>
        </div>

        <hr class="mb-4">
     <div class="row">
      <div class="col-md-8">
      <form id="input_data_form" class="needs-validation" novalidate>
        <div class="row">
          <div class="col-md-6 mb-3">
            <label for="nrow">rows</label>
            <input type="number" class="form-control" id="nrow" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
          <div class="col-md-6 mb-3">
            <label for="ncol">columns</label>
            <input type="number" class="form-control" id="ncol" min="0" step="1" placeholder="" value="2" required>
            <div class="invalid-feedback">
              please provide an integer
            </div>
          </div>
        </div>
        <div class="form-row text-center">
          <div class="col-12">
              <button id="calculate" type="submit" class="btn btn-primary">Calculate!</button>
          </div>
       </div>
      </form>
      </div>
        <div class="col-md-4">

          <div class="header">
            <h5 class="text-muted">Plot results</h5>
          </div>
          <div id="plot_content">

          </div>
        </div>
     </div>

      <hr class="mb-4">

      <table id="a_nice_table" class="table table-striped"></table>

    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js" type="text/javascript"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-0.12.15.min.js"></script>
    <script src="http://cdn.bokeh.org/bokeh/release/bokeh-widgets-0.12.15.min.js"></script>
    <script type="text/javascript">
      $(document).ready(function() {
        var table = null;
        $('#input_data_form').submit(function (event) {
            event.preventDefault();
            if ($('#input_data_form')[0].checkValidity() === false) {
                event.stopPropagation();
                if (table !== null) {
                    table.destroy();
                    table = null;
                    $("#a_nice_table").empty();
                  }
            } else {
                $.getJSON('/_get_table', {
                    nrow: $("#nrow").val(),
                    ncol: $("#ncol").val()

                  }, function(data) {

                  $('#plot_content').html(data.html_plot);

                  if (table !== null) {
                    table.destroy();
                    table = null;
                    $("#a_nice_table").empty();
                  }
                  table = $("#a_nice_table").DataTable({
                    data: data.my_table,
                    columns: data.columns

                  });
                });
            return false;
            }
            $('#input_data_form').addClass('was-validated');
        });

      });
    </script>
  </body>

There might be better options where the plot is really just updated instead of redrawn completely as in this solution, but that gets the job done for now.

like image 27
Cleb Avatar answered Nov 15 '22 22:11

Cleb