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"

  • https://github.com/bokeh/bokeh/blob/master/examples/app/stocks (Before run we have to launch download_sample_data.py to get datas.)

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

  • if data sources are 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"

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>


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 .

Finally I put <script src="/static/plugins_node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"></script> in frontend/public/index.html instead of this script url and window.Bokeh. ... in frontend/src/pages/ProdSinusPage.vue.

Links

  • https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
  • https://docs.bokeh.org/en/latest/docs/user_guide/embed.html
  • Flask + Bokeh AjaxDataSource
  • https://discourse.bokeh.org/t/node12-import-error-bokeh-2-0/5061
  • https://github.com/bokeh/bokeh/issues/10658
  • How to store Node.js deployment settings/configuration files?
  • Using Environment Variables with Vue.js
  • https://github.com/vuejs/vuex/tree/4.0/examples/composition/shopping-cart
  • https://www.digitalocean.com/community/tutorials/how-to-build-a-shopping-cart-with-vue-3-and-vuex
  • https://github.com/vuejs/vuex/tree/4.0/examples/composition
  • https://www.codimth.com/blog/web/vuejs/how-use-composition-api-vuejs-3
  • https://markus.oberlehner.net/blog/vue-3-composition-api-vs-options-api/

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')

    @app.route('/static/plugins_node_modules/<path:path>')
    def send_plugins_(path):
        print(app.app_dir_root)
        print(os.path.join(app.app_dir_root, 'frontend', 'node_modules'))
        return send_from_directory((os.path.join(app.app_dir_root, 'frontend', 'node_modules')), path)

    # 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 "./../node_modules/bulma/css/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";

//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;
  eval(temp1.script);
}
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"),
      },
    },
    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",
        },
      },

      "/static/plugins_node_modules": {
        target: "http://localhost:" + process.env.CONFIG_PORTFLASK + "/",
        changeOrigin: true,
        pathRewrite: {
          "^/static/plugins_node_modules": "/static/plugins_node_modules/",
        },
      },
    },
  },

  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;