pythonflaskbokehvuejs3vuex4

Flask with `import bokeh` with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"


TL;DR

I am beginner in bokeh.

I've read https://docs.bokeh.org or others examples in stackoverflow and github but I don't find examples in Flask with import bokeh with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"

All examples or tutorials are for bokeh server or bokeh server embedded in Flask.

09/09/2021: I've done a POC with flask, bokeh, vue3,vuex4, composition-api: https://github.com/philibe/FlaskVueBokehPOC. I clean my last auto answer and create a new one with a POC as tutorial.

The issue

I've started with bokeh server example below, modified by me with interactive with shared data sources, but I have issues to convert to Flask with import bokeh with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"

The expected answer of the issue is ultimately to have an example in Flask with import bokeh with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"

Initial bokeh server example with my modification : it works.

bokeh serve main.py --allow-websocket-origin=192.168.1.xxx:5006

from functools import lru_cache
from os.path import dirname, join

import numpy as np

import pandas as pd

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, PreText, Select
from bokeh.plotting import figure

import logging
import json
#log = logging.getLogger('bokeh')

LOG_FORMAT = "%(levelname)s %(asctime)s - %(message)s"
file_handler = logging.FileHandler(filename='test.log', mode='w')
file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
logger = logging.getLogger('toto')
logger.addHandler(file_handler)
logger.setLevel(logging.DEBUG)

logger.info('Hello there')


DATA_DIR = join(dirname(__file__), 'daily')

DEFAULT_TICKERS = ['AAPL', 'GOOG', 'INTC', 'BRCM', 'YHOO']

def nix(val, lst):
    return [x for x in lst if x != val]

@lru_cache()
def load_ticker(ticker):
    fname = join(DATA_DIR, 'table_%s.csv' % ticker.lower())
    data = pd.read_csv(fname, header=None, parse_dates=['date'],
                       names=['date', 'foo', 'o', 'h', 'l', 'c', 'v'])
    data = data.set_index('date')
    return pd.DataFrame({ticker: data.c, ticker+'_returns': data.c.diff()})

@lru_cache()
def get_data(t1, t2):
    df1 = load_ticker(t1)
    df2 = load_ticker(t2)
    data = pd.concat([df1, df2], axis=1)
    data = data.dropna()
    data['t1'] = data[t1]
    data['t2'] = data[t2]
    data['t1_returns'] = data[t1+'_returns']
    data['t2_returns'] = data[t2+'_returns']
    return data

# set up widgets

stats = PreText(text='', width=500)
ticker1 = Select(value='AAPL', options=nix('GOOG', DEFAULT_TICKERS))
ticker2 = Select(value='GOOG', options=nix('AAPL', DEFAULT_TICKERS))

# set up plots

source = ColumnDataSource(data=dict(date=[], t1=[], t2=[], t1_returns=[], t2_returns=[]))
source_static = ColumnDataSource(data=dict(date=[], t1=[], t2=[], t1_returns=[], t2_returns=[]))
tools = 'pan,wheel_zoom,xbox_select,reset'

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

corr = figure(width=350, height=350,
              tools='pan,wheel_zoom,box_select,reset', tooltips=TOOLTIPS)
corr.circle('t1_returns', 't2_returns', size=2, source=source,
            selection_color="orange", alpha=0.6, nonselection_alpha=0.1, selection_alpha=0.4)

ts1 = figure(width=900, height=200, tools=tools, x_axis_type='datetime', active_drag="xbox_select", tooltips=TOOLTIPS)

ts1.line('date', 't1', source=source_static)
ts1.circle('date', 't1', size=1, source=source, color=None, selection_color="orange")

ts2 = figure(width=900, height=200, tools=tools, x_axis_type='datetime', active_drag="xbox_select", tooltips=TOOLTIPS)

#logger.info(repr( ts1.x_range))

ts2.x_range = ts1.x_range
ts2.line('date', 't2', source=source_static)
ts2.circle('date', 't2', size=1, source=source, color=None, selection_color="orange")

ts2.vbar(x='date', top='t1', source=source_static,width = .9)

# set up callbacks

def ticker1_change(attrname, old, new):
    ticker2.options = nix(new, DEFAULT_TICKERS)
    update()

def ticker2_change(attrname, old, new):
    ticker1.options = nix(new, DEFAULT_TICKERS)
    update()

def update(selected=None):
    t1, t2 = ticker1.value, ticker2.value

    df = get_data(t1, t2)
    data = df[['t1', 't2', 't1_returns', 't2_returns']]
    source.data = data
    source_static.data = data

    update_stats(df, t1, t2)

    corr.title.text = '%s returns vs. %s returns' % (t1, t2)
    ts1.title.text, ts2.title.text = t1, t2

def update_stats(data, t1, t2):
    stats.text = str(data[[t1, t2, t1+'_returns', t2+'_returns']].describe())

ticker1.on_change('value', ticker1_change)
ticker2.on_change('value', ticker2_change)

def selection_change(attrname, old, new):
    t1, t2 = ticker1.value, ticker2.value
    data = get_data(t1, t2)
    selected = source.selected.indices
    if selected:
        data = data.iloc[selected, :]
    update_stats(data, t1, t2)

source.selected.on_change('indices', selection_change)

# set up layout
widgets = column(ticker1, ticker2, stats)
main_row = row(corr, widgets)
series = column(ts1, ts2)
layout = column(main_row, series)

# initialize
update()

curdoc().add_root(layout)
curdoc().title = "Stocks"

Bokeh server source (badly) converted to Flask with import bokeh with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"

python app_so.py -> http://192.168.1.xxx:5007/stock1

I read that the common fix is to have different sources but I want shared sources like in the bokeh server example I modified.

And in second time I have this warning below : are Js callbacks mandatory in Flask for bokeh ?

WARNING:bokeh.embed.util: You are generating standalone HTML/JS output, but trying to use real Python callbacks (i.e. with on_change or on_event). This combination cannot work.

Only JavaScript callbacks may be used with standalone output. For more information on JavaScript callbacks with Bokeh, see:

https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html

app_so.py

from flask import Flask, Response, render_template, jsonify, request, json

from bokeh.embed import components
import bokeh.embed as embed

from bokeh.plotting import figure
from bokeh.resources import INLINE
from bokeh.embed import json_item

from flask_debugtoolbar import DebugToolbarExtension

from werkzeug.utils import import_string

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

import numpy as np
import json



from functools import lru_cache
from os.path import dirname, join

import numpy as np

import pandas as pd

#from bokeh.io import curdoc
#from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, PreText, Select

import json

app = Flask(__name__)

app.debug = True
app.config['SECRET_KEY'] = 'xxxxx'
toolbar = DebugToolbarExtension()
toolbar.init_app(app)



tools = 'pan,wheel_zoom,xbox_select,reset'

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

DATA_DIR = join(dirname(__file__), 'daily')

DEFAULT_TICKERS = ['AAPL', 'GOOG', 'INTC', 'BRCM', 'YHOO']

def nix(val, lst):
    return [x for x in lst if x != val]

@lru_cache()
def load_ticker(ticker):
    fname = join(DATA_DIR, 'table_%s.csv' % ticker.lower())
    data = pd.read_csv(fname, header=None, parse_dates=['date'],
                       names=['date', 'foo', 'o', 'h', 'l', 'c', 'v'])
    data = data.set_index('date')
    return pd.DataFrame({ticker: data.c, ticker+'_returns': data.c.diff()})

@lru_cache()
def get_data(t1, t2):
    df1 = load_ticker(t1)
    df2 = load_ticker(t2)
    data = pd.concat([df1, df2], axis=1)
    data = data.dropna()
    data['t1'] = data[t1]
    data['t2'] = data[t2]
    data['t1_returns'] = data[t1+'_returns']
    data['t2_returns'] = data[t2+'_returns']
    return data



# set up callbacks

def ticker1_change(attrname, old, new):
    ticker2.options = nix(new, DEFAULT_TICKERS)
    update()

def ticker2_change(attrname, old, new):
    ticker1.options = nix(new, DEFAULT_TICKERS)
    update()

def update(source,source_static,stats, ticker1, ticker2,corr, ts1, ts2,selected=None):
    t1, t2 = ticker1.value, ticker2.value

    df = get_data(t1, t2)
    data = df[['t1', 't2', 't1_returns', 't2_returns']]
    source.data = data
    source_static.data = data

    update_stats(stats,df, t1, t2)

    corr.title.text = '%s returns vs. %s returns' % (t1, t2)
    ts1.title.text, ts2.title.text = t1, t2

def update_stats(stats,data, t1, t2):
    stats.text = str(data[[t1, t2, t1+'_returns', t2+'_returns']].describe())



def selection_change(attrname, old, new):
    t1, t2 = ticker1.value, ticker2.value
    data = get_data(t1, t2)
    selected = source.selected.indices
    if selected:
        data = data.iloc[selected, :]
    update_stats(data, t1, t2)
    


def init_data():
  
  
  source = ColumnDataSource(data=dict(date=[], t1=[], t2=[], t1_returns=[], t2_returns=[]))
  source_static = ColumnDataSource(data=dict(date=[], t1=[], t2=[], t1_returns=[], t2_returns=[]))
  # set up widgets

  stats = PreText(text='', width=500)
  ticker1 = Select(value='AAPL', options=nix('GOOG', DEFAULT_TICKERS))
  ticker2 = Select(value='GOOG', options=nix('AAPL', DEFAULT_TICKERS))

  ticker1.on_change('value', ticker1_change)
  ticker2.on_change('value', ticker2_change)    
  
  # set up plots  

  source.selected.on_change('indices', selection_change)


  corr = figure(width=350, height=350,
                tools='pan,wheel_zoom,box_select,reset', tooltips=TOOLTIPS, name='CORR')
  corr.circle('t1_returns', 't2_returns', size=2, source=source,
              selection_color="orange", alpha=0.6, nonselection_alpha=0.1, selection_alpha=0.4)

  ts1 = figure(width=900, height=200, tools=tools, x_axis_type='datetime', active_drag="xbox_select", tooltips=TOOLTIPS, name='TS1')

  # For the lines below: I get  # 
  # - if data source is different, everything is ok, 
  # - if datas are yet loaded in the figure : "RuntimeError: Models must be owned by only a single document, Selection(id='1043', ...) is already in a doc"
  ts1.line('date', 't1', source=source_static)
  ts1.circle('date', 't1', size=1, source=source_static, color=None, selection_color="orange")

  ts2 = figure(width=900, height=200, tools=tools, x_axis_type='datetime', active_drag="xbox_select", tooltips=TOOLTIPS, name='TS2')

  #logger.info(repr( ts1.x_range))

  ts2.x_range = ts1.x_range
  ts2.line('date', 't2', source=source_static)
  ts2.circle('date', 't2', size=1, source=source, color=None, selection_color="orange")


  ts2.vbar(x='date', top='t1', source=source_static,width = .9)
  return source,source_static,stats, ticker1, ticker2,corr, ts1, ts2
  
  # cwidgets = column(ticker1, ticker2, stats)
  # cmain_row = row(corr, widgets)
  # cseries = column(ts1, ts2)
  # clayout = column(main_row, series)

  # curdoc().add_root(layout)
  # curdoc().title = "Stocks"
  

@app.route('/stock1')
def stock1():
     
    fig = figure(plot_width=600, plot_height=600)
    fig.vbar(
        x=[1, 2, 3, 4],
        width=0.5,
        bottom=0,
        top=[1.7, 2.2, 4.6, 3.9],
        color='navy'
    )

    source,source_static,stats, ticker1, ticker2,corr, ts1, ts2= init_data()
    # initialize
    update(source,source_static,stats, ticker1, ticker2,corr, ts1, ts2)

    # grab the static resources
    js_resources = INLINE.render_js()
    css_resources = INLINE.render_css()

    # render template

    script01, div01 = components(ticker1)
    script02, div02 = components(ticker2)
    script00, div00 = components(stats)

    script0, div0 = components(corr)

    script1, div1 = components(ts1)
    """           
    script2, div2 = components(ts2)
    """
    
    html = render_template(
        'index2.html',

        plot_script01=script01,
        plot_div01=div01,
      
        plot_script02=script02,
        plot_div02=div02,
      
        plot_script00=script00,
        plot_div00=div00,
      
       
        plot_script0=script0,
        plot_div0=div0,

        plot_script1=script1,
        plot_div1=div1,
      
        # plot_script2=script2,
        # plot_div2=div2,
        
      
        js_resources=js_resources,
        css_resources=css_resources,
    )
    return (html)

  


if __name__ == '__main__':
    PORT = 5007
    app.run(host='0.0.0.0', port=PORT, debug=True)

index2.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>Embed Demo</title>
    {{ js_resources|indent(4)|safe }}
    {{ css_resources|indent(4)|safe }}
    
    {{ plot_script00|indent(4)|safe }}
    {{ plot_script01|indent(4)|safe }}    
    {{ plot_script02|indent(4)|safe }}     

    {{ plot_script0|indent(4)|safe }}
       
    {{ plot_script1|indent(4)|safe }}    
    {# 
    {{ plot_script2|indent(4)|safe }}    
    
    #}
  </head>
  <body>

    {{ plot_div01|indent(4)|safe }}
    {{ plot_div02|indent(4)|safe }}        
    {{ plot_div00|indent(4)|safe }}    
    
    
    {{ plot_div0|indent(4)|safe }}

    {{ plot_div1|indent(4)|safe }}
    {# 
    {{ plot_div2|indent(4)|safe }}    
    #}
  </body>
</html>


Solution

  • 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.

    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

    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")
        .replaceAll("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.)

    //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);
    
      configureWebpack: {
        resolve: {
          extensions: [".js", ".vue", ".json", ".scss"],
          alias: {
            "bokeh.min.js": path.join(
              __dirname,
              "/node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"
            ),
          },
        },
    

    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