Skip to content

Here is a POC with `import bokeh` without external bokeh server and with vue (vue3,vuex4, composition-api) because I didn't found a tutorial for my needs.

Notifications You must be signed in to change notification settings

philibe/FlaskVueBokehPOC

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This POC is coming from StackOverflow post

My answer updated a little for github

Here is a POC with import bokeh without external bokeh server and with vue (vue3,vuex4, composition-api) because I didn't found a tutorial for my needs.

There are 2 bokeh graphs linked by a lasso with python js_on_change() via python components({}) which generate a js script with Bokeh.embed.embed_items() inside.

  • Flask

    • api datas
    • api Python Bokeh functions
  • VueJs

    • Vue 3
    • vuex 4
    • management of data feedback in a <ol> <li> list and 2 bokeh graphs in a template view via API composition

Look at https://github.com/philibe/FlaskVueBokehPOC for the source code detail.

Import issue

Because of discourse.bokeh.org: Node12 import error bokeh 2.0 I call bokehjs by the DOM javascript window.Bokeh. ... in frontend/src/pages/ProdSinusPage.vue.

I've seen this Github Issue #10658 (opened):[FEATURE] Target ES5/ES6 with BokehJS .

edit 23/09/2022, 31/10/2022: See below edit some

  • string replacement
  • webpack aliases
  • and mainly new part "Importation of Bokeh".

end of edit

Links

Code abstract

server/config.py

SECRET_KEY = 'GITHUB6202f13e27c5'
PORT_FLASK_DEV = 8071
PORT_FLASK_PROD = 8070
PORT_NODE_DEV = 8072

server/app.py

from flask import (
    Flask,
    jsonify,
    request,
    render_template,
    flash,
    redirect,
    url_for,
    session,
    send_from_directory,
    # abort,
)

from bokeh.layouts import row, column, gridplot, widgetbox

from flask_cors import CORS
import uuid
import os


from bokeh.embed import json_item, components
from bokeh.plotting import figure, curdoc
from bokeh.models.sources import AjaxDataSource, ColumnDataSource


from bokeh.models import CustomJS

# from bokeh.models.widgets import Div

bokeh_tool_tips = [
    ("index", "$index"),
    ("(x,y)", "($x, $y)"),
    # ("desc", "@desc"),
]

bokeh_tool_list = ['pan,wheel_zoom,lasso_select,reset']

import math
import json


from flask_debugtoolbar import DebugToolbarExtension

from werkzeug.utils import import_string

from werkzeug.serving import run_simple
from werkzeug.middleware.dispatcher import DispatcherMiddleware


def create_app(PROD, DEBUG):

    app = Flask(__name__)

    app.dir_app = os.path.abspath(os.path.dirname(__file__))
    app.app_dir_root = os.path.dirname(app.dir_app)
    app.app_dir_nom = os.path.basename(app.dir_app)

    print(app.dir_app)
    print(app.app_dir_root)
    print(app.app_dir_nom)

    if not PROD:
        CORS(app, resources={r'/*': {'origins': '*'}})
        template_folder = '../frontend/public'
        static_url_path = 'static'
        static_folder = '../frontend/public/static'

    else:
        template_folder = '../frontend/dist/'
        static_url_path = 'static'
        static_folder = '../frontend/dist/static'

    app.template_folder = template_folder
    app.static_url_path = static_url_path
    app.static_folder = static_folder

    # Ă  rajouter
    # app.wsgi_app = ReverseProxied(app.wsgi_app, script_name='/' + app.app_dir_nom)

    app.debug = DEBUG

    app.config.from_pyfile('config.py')
    if DEBUG:
        toolbar = DebugToolbarExtension()
        toolbar.init_app(app)

    @app.before_first_request
    def initialize():
        session.clear()
        if not session.get('x'):
            session['x'] = 0
        if not session.get('y'):
            session['y'] = 0
        if not session.get('HistoryArray'):
            session['HistoryArray'] = [{'x': None, 'y': None}]

    @app.route('/')
    def index():
        VariableFlask = 'VariableFlaskRendered'
        return render_template('index.html', VariableFlask=VariableFlask)

    @app.route('/favicon.ico')
    def favicon():
        return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/x-icon')

    # https://stackoverflow.com/questions/37083998/flask-bokeh-ajaxdatasource
    # https://github.com/bokeh/bokeh/blob/main/examples/embed/json_item.py

    @app.route("/api/datasinus/<operation>", methods=['GET', 'POST'])
    def get_x(operation):
        if not session.get('x'):
            session['x'] = 0
        if not session.get('y'):
            session['y'] = 0
        if not session.get('HistoryArray'):
            session['HistoryArray'] = [{'x': None, 'y': None}]

        # global x, y
        if operation == 'increment':
            session['x'] = session['x'] + 0.1

        session['y'] = math.sin(session['x'])

        if operation == 'increment':
            session['HistoryArray'].append({'x': session['x'], 'y': session['y']})
            return jsonify(x=[session['x']], y=[session['y']])
        else:
            response_object = {'status': 'success'}
            # malist[-10:] last n elements
            # malist[::-1] reversing using list slicing
            session['HistoryArray'] = session['HistoryArray'][-10:]
            response_object['sinus'] = session['HistoryArray'][::-1]
            return jsonify(response_object)

    @app.route("/api/bokehinlinejs", methods=['GET', 'POST'])
    def simple():
        streaming = True

        s1 = AjaxDataSource(data_url="/api/datasinus/increment", polling_interval=1000, mode='append')

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

        s2 = ColumnDataSource(data=dict(x=[], y=[]))

        s1.selected.js_on_change(
            'indices',
            CustomJS(
                args=dict(s1=s1, s2=s2),
                code="""
            var inds = cb_obj.indices;
            var d1 = s1.data;
            var d2 = s2.data;
            d2['x'] = []
            d2['y'] = []
            for (var i = 0; i < inds.length; i++) {
                d2['x'].push(d1['x'][inds[i]])
                d2['y'].push(d1['y'][inds[i]])
            }
            s2.change.emit();
            
            """,
            ),
        )

        p1 = figure(
            x_range=(0, 10),
            y_range=(-1, 1),
            plot_width=400,
            plot_height=400,
            title="Streaming, take lasso to copy points (refresh after)",
            tools=bokeh_tool_list,
            tooltips=bokeh_tool_tips,
            name="p1",
        )
        p1.line('x', 'y', source=s1, color="blue", selection_color="green")
        p1.circle('x', 'y', size=1, source=s1, color=None, selection_color="red")

        p2 = figure(
            x_range=p1.x_range,
            y_range=(-1, 1),
            plot_width=400,
            plot_height=400,
            tools=bokeh_tool_list,
            title="Watch here catched points",
            tooltips=bokeh_tool_tips,
            name="p2",
        )
        p2.circle('x', 'y', source=s2, alpha=0.6)

        response_object = {}
        response_object['gr'] = {}

        script, div = components({'p1': p1, 'p2': p2}, wrap_script=False)
        response_object['gr']['script'] = script
        response_object['gr']['div'] = div
        return response_object

    return app


if __name__ == '__main__':
    from argparse import ArgumentParser

    parser = ArgumentParser()
    parser.add_argument('--PROD', action='store_true')
    parser.add_argument('--DEBUG', action='store_true')
    args = parser.parse_args()

    DEBUG = args.DEBUG
    PROD = args.PROD

    print('DEBUG=', DEBUG)
    print('PROD=', PROD)

    app = create_app(PROD=PROD, DEBUG=DEBUG)

    if not PROD:
        PORT = app.config["PORT_FLASK_DEV"]
    else:
        PORT = app.config["PORT_FLASK_PROD"]

    if DEBUG:
        app.run(host='0.0.0.0', port=PORT, debug=DEBUG)

    else:
        from waitress import serve

        serve(app, host="0.0.0.0", port=PORT)

frontend/src/main.js

import { createApp, prototype } from "vue";
import store from "@/store/store.js";
import App from "@/App.vue";
import router from "@/router/router.js";
import "bulma.css";

// https://v3.vuejs.org/guide/migration/filters.html#migration-strategy
// "Filters are removed from Vue 3.0 and no longer supported"
// Vue.filter('currency', currency)

const app = createApp(App).use(store).use(router);

app.mount("#app");

frontend/src/pages/ProdSinusPage.vue

<style>
  [..]
</style>
<template>
  <div class="row" style="width: 60%">
    <div id="bokeh_ch1" class="column left"></div>
    <div class="column middle">
      <ul>
        <li v-for="data in datasinus" :key="data.x">
          [[ currency(data.x,'',2) ]] - [[currency(data.y,'',2) ]]
        </li>
      </ul>
    </div>
    <div id="bokeh_ch2" class="column right"></div>
  </div>
</template>

<script setup>
// https://v3.vuejs.org/api/sfc-script-setup.html
import { computed, onBeforeUnmount } from "vue";
import { useStore } from "vuex";
import { currency } from "@/currency";

//var Bokeh = require("bokeh.min.js");
import * as Bokeh from "bokeh.min.js";

window.Bokeh = Bokeh.Bokeh;

//https://github.com/vuejs/vuex/tree/4.0/examples/composition/shopping-cart

const store = useStore();

const bokehinlinejs = computed(() => store.state.modprodsinus.bokehinlinejs);

async function get1stJsonbokeh() {
  const promise = new Promise((resolve /*, reject */) => {
    setTimeout(() => {
      return resolve(bokehinlinejs.value);
    }, 1001);
  });
  let result = await promise;

  var temp1 = result.gr;
  document.getElementById("bokeh_ch1").innerHTML = temp1.div.p1;
  document.getElementById("bokeh_ch2").innerHTML = temp1.div.p2;
  let newscript = temp1.script // from script from bokeh.embed.components()
    .replace("Bokeh.safely", "window.Bokeh.safely")
    .replaceAll("root.Bokeh", "window.Bokeh")
    .replace("attempts > 100", "attempts > 1000");
  eval(newscript);
}
get1stJsonbokeh();

var productCheckInterval = null;
const datasinus = computed(() => store.state.modprodsinus.datasinus);

//console.log(datasinus)

async function getDataSinusPolling() {
  const promise = new Promise((resolve /*, reject */) => {
    setTimeout(() => {
      resolve(datasinus);
    }, 1001);
  });
  let result = await promise;

  clearInterval(productCheckInterval);
  productCheckInterval = setInterval(() => {
    store.dispatch("modprodsinus/GetDataSinus");
    //console.log(productCheckInterval)
  }, 1000);
}

getDataSinusPolling();

const beforeDestroy = onBeforeUnmount(() => {
  clearInterval(productCheckInterval);
  console.log("beforeDestroy");
});

store.dispatch("modprodsinus/GetBokehinlinejs");
</script>

frontend/src/api/apisinus.js

import axios from "axios";

export default {
  apiGetBokehinlinejs(callback) {
    axios
      .get("/api/bokehinlinejs")
      .then((response) => {
        console.log(response.data);
        callback(response.data);
      })
      .catch((err) =>
        console.log(
          (process.env.NODE_ENV || "dev") == "build"
            ? err.message
            : JSON.stringify(err)
        )
      );
  },
  apiGetDatasinus(callback) {
    axios
      .get("/api/datasinus/read")
      .then((response) => {
        //console.log(response.data)
        callback(response.data.sinus);
      })
      .catch((err) =>
        console.log(
          (process.env.NODE_ENV || "dev") == "build"
            ? err.message
            : JSON.stringify(err)
        )
      );
  },
};

frontend/src/store/modules/modprodsinus/modprodsinus.js

import apisinus from "@/api/apisinus.js";

// initial state
const state = {
  bokehinlinejs: [],
  datasinus: [],
};

const getters = {
  datasinus: (state) => {
    return state.datasinus;
  },
};

// https://github.com/vuejs/vuex/tree/4.0/examples/composition/shopping-cart

// actions
const actions = {
  GetBokehinlinejs({ commit }) {
    apisinus.apiGetBokehinlinejs((bokehinlinejs) => {
      commit("setBokehinlinejs", bokehinlinejs);
    });
  },
  GetDataSinus({ commit }) {
    apisinus.apiGetDatasinus((datasinus) => {
      commit("setDataSinus", datasinus);
    });
  },
};

// mutations
const mutations = {
  setBokehinlinejs(state, bokehinlinejs) {
    state.bokehinlinejs = bokehinlinejs;
  },
  setDataSinus(state, datasinus) {
    state.datasinus = datasinus;
  },
};

const modprodsinus = {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};

export default modprodsinus;

frontend/src/router/router.js

import { createRouter, createWebHistory } from "vue-router";
import Home from "@/pages/Home.vue";
import About from "@/pages/About.vue";
import About2Comp from "@/pages/About2Comp.vue";

import prodsinuspage from "@/pages/ProdSinusPage.vue";

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component: About,
  },
  {
    path: "/about2",
    name: "About2",
    component: About2Comp,
  },
  {
    path: "/prodsinuspage",
    name: "prodsinuspage",
    component: prodsinuspage,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

frontend/src/store/store.js

import { createStore } from "vuex";
import modprodsinus from "./modules/modprodsinus/modprodsinus.js";

// https://www.digitalocean.com/community/tutorials/how-to-build-a-shopping-cart-with-vue-3-and-vuex

export default createStore({
  modules: {
    modprodsinus,
  },
});

frontend/ package.json, vue_node_serve.js,vue_node_build.js

package.json:
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "NODE_ENV='dev' node vue_node_serve.js ",
    "build": "NODE_ENV='build' node vue_node_build.js ",
    "lint": "vue-cli-service lint"
  },
[..]
frontend/vue_node_serve.js:
const config = require("./config");

require("env-dot-prop").set("CONFIG.PORTFLASK", config.port_flask);
require("env-dot-prop").set("CONFIG.PORTNODEDEV", config.port_node_dev);
require("child_process").execSync(
  "vue-cli-service serve --port " + config.port_node_dev,
  { stdio: "inherit" }
);
frontend/vue_node_build.js:
const config = require("./config");
require("env-dot-prop").set("CONFIG.PORTFLASK", config.port_flask);
require("child_process").execSync("vue-cli-service build", {
  stdio: "inherit",
});

frontend/vue.config.js

// https://stackoverflow.com/questions/50828904/using-environment-variables-with-vue-js/57295959#57295959
// https://www.fatalerrors.org/a/vue3-explains-the-configuration-of-eslint-step-by-step.html

const webpack = require("webpack");

const env = process.env.NODE_ENV || "dev";

const path = require("path");

module.exports = {
  indexPath: "index.html",
  assetsDir: "static/app/",

  configureWebpack: {
    resolve: {
      extensions: [".js", ".vue", ".json", ".scss"],
      alias: {
        styles: path.resolve(__dirname, "src/assets/scss"),
        "bokeh.min.js": path.join(
           __dirname,
           "/node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"
        ),
        "bulma.css": path.join(__dirname, "/node_modules/bulma/css/bulma.css"),
      },
    },
    plugins: [
      new webpack.DefinePlugin({
        // allow access to process.env from within the vue app
        "process.env": {
          NODE_ENV: JSON.stringify(env),
          CONFIG_PORTFLASK: JSON.stringify(process.env.CONFIG_PORTFLASK),
          CONFIG_PORTNODEDEV: JSON.stringify(process.env.CONFIG_PORTNODEDEV),
        },
      }),
    ],
  },

  devServer: {
    watchOptions: {
      poll: true,
    },
    proxy: {
      "/api": {
        target: "http://localhost:" + process.env.CONFIG_PORTFLASK + "/",
        changeOrigin: true,
        pathRewrite: {
          "^/api": "/api",
        },
      },
    },
  },

  chainWebpack: (config) => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        options.compilerOptions = {
          delimiters: ["[[", "]]"],
        };
        return options;
      });
  },

  lintOnSave: true,
};

// https://prettier.io/docs/en/install.html
// https://www.freecodecamp.org/news/dont-just-lint-your-code-fix-it-with-prettier/

frontend/config.js

// https://stackoverflow.com/questions/5869216/how-to-store-node-js-deployment-settings-configuration-files
// https://stackoverflow.com/questions/41767409/read-from-file-and-find-specific-lines/41767642#41767642

function getValueByKey(text, key) {
  var regex = new RegExp("^" + key + "\\s{0,1}=\\s{0,1}(.*)$", "m");
  var match = regex.exec(text);
  if (match) {
    return match[1];
  } else {
    return null;
  }
}

function getValueByKeyInFilename(key, filename) {
  return getValueByKey(
    require("fs").readFileSync(filename, { encoding: "utf8" }),
    key
  );
}

const python_config_filename = "../server/config.py";

const env = process.env.NODE_ENV || "dev";

var config_temp = {
  dev: {
    port_flask: getValueByKeyInFilename(
      "PORT_FLASK_DEV",
      python_config_filename
    ),
    port_node_dev: getValueByKeyInFilename(
      "PORT_NODE_DEV",
      python_config_filename
    ),
  },
  build: {
    port_flask: getValueByKeyInFilename(
      "PORT_FLASK_PROD",
      python_config_filename
    ),
  },
};
var config = {
  ...config_temp[env],
};

module.exports = config;

"Importation of Bokeh";`

import * as Bokeh from "bokeh.min.js"; (and require ('bokeh.min.js') ) work with VueJS 3 with 2 warnings in both cases :)

At least with js script from Python Embedding Bokeh content script, div = bokeh.embed.components({'p1': p1, 'p2': p2}, wrap_script=False).

(Don't forget, in the graph, to scroll x to the value displayed in the middle text to see the graph.)

  • frontend/src/pages/ProdSinusPage.vue (Here is my update):
//var Bokeh = require("bokeh.min.js");
import * as Bokeh from "bokeh.min.js";
window.Bokeh = Bokeh.Bokeh;
....
  let newscript = temp1.script // from script from bokeh.embed.components()
    .replace("Bokeh.safely", "window.Bokeh.safely")
    .replaceAll("root.Bokeh", "window.Bokeh")
    .replace("attempts > 100", "attempts > 1000");
  eval(newscript);
  • frontend/vue.config.js (nearly same as my above comment)
  configureWebpack: {
    resolve: {
      extensions: [".js", ".vue", ".json", ".scss"],
      alias: {
        "bokeh.min.js": path.join(
          __dirname,
          "/node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"
        ),
      },
    },
  • without module.exports = { ...... devServer: { proxy: { "/static/plugins_node_modules":
  • without @app.route() in the Flask side (same as my above comment)

Despite of

WARNING Compiled with 2 warnings warning in ./node_modules/@bokeh/bokehjs/build/js/bokeh.min.js Critical dependency: require function is used in a way in which dependencies cannot be statically extracted warning in ./node_modules/@bokeh/bokehjs/build/js/bokeh.min.js Critical dependency: the request of a dependency is an expression

About

Here is a POC with `import bokeh` without external bokeh server and with vue (vue3,vuex4, composition-api) because I didn't found a tutorial for my needs.

Topics

Resources

Stars

Watchers

Forks