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;