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.
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"
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"
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"
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>
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
VueJs
<ol> <li>
list and 2 bokeh graphs in a template view via API compositionLook at https://github.com/philibe/FlaskVueBokehPOC for the source code detail.
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
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;
import * as Bokeh from "bokeh.min.js";
(and require ('bokeh.min.js')
) work with VueJS 3 with 2 warnings in both cases :)
At least with js script from Python Embedding Bokeh content script, div = bokeh.embed.components({'p1': p1, 'p2': p2}, wrap_script=False)
.
(Don't forget, in the graph, to scroll x to the value displayed in the middle text to see the graph.)
frontend/src/pages/ProdSinusPage.vue
(Here is my update)://var Bokeh = require("bokeh.min.js");
import * as Bokeh from "bokeh.min.js";
window.Bokeh = Bokeh.Bokeh;
....
let newscript = temp1.script // from script from bokeh.embed.components()
.replace("Bokeh.safely", "window.Bokeh.safely")
.replaceAll("root.Bokeh", "window.Bokeh")
.replace("attempts > 100", "attempts > 1000");
eval(newscript);
frontend/vue.config.js
(nearly same as my above comment) configureWebpack: {
resolve: {
extensions: [".js", ".vue", ".json", ".scss"],
alias: {
"bokeh.min.js": path.join(
__dirname,
"/node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"
),
},
},
module.exports = { ...... devServer: { proxy: { "/static/plugins_node_modules"
:@app.route()
in the Flask side (same as my above comment)Despite of
WARNING Compiled with 2 warnings warning in ./node_modules/@bokeh/bokehjs/build/js/bokeh.min.js Critical dependency: require function is used in a way in which dependencies cannot be statically extracted warning in ./node_modules/@bokeh/bokehjs/build/js/bokeh.min.js Critical dependency: the request of a dependency is an expression