Flask with `import bokeh` 带有 2 个散景图,没有外部散景服务器而不是 "Models must be owned by only a single document"
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
我是散景初学者。
我已经在 Whosebug 和 github 中阅读了 https://docs.bokeh.org 或其他示例,但我没有在 Flask 中找到带有 import bokeh
和 2 个没有外部散景服务器的散景图的示例“模型只能由一个文档拥有”
所有示例或教程均适用于散景服务器或 Flask 中嵌入的散景服务器。
09/09/2021:我用 flask、bokeh、vue3、vuex4、composition-api:https://github.com/philibe/FlaskVueBokehPOC 完成了 POC。我清理了我的上一个自动回答并创建了一个带有 POC 作为教程的新自动回答。
问题
我从下面的散景服务器示例开始,由我修改并与共享数据源交互,但我在转换为 Flask 时遇到问题 import bokeh
有 2 个散景图,没有外部散景服务器,而不是“模型只能由一个文档拥有
- https://github.com/bokeh/bokeh/blob/master/examples/app/stocks(在 运行 之前我们必须启动
download_sample_data.py
来获取数据。)
这个问题的预期答案最终是在 Flask 中有一个示例 import bokeh
有 2 个没有外部散景服务器的散景图,而不是“模型必须仅由一个文档拥有"
经过我修改的初始散景服务器示例:有效。
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 服务器源(严重)转换为 Flask import bokeh
带有 2 个散景图,没有外部散景服务器,而不是“模型必须仅由一个文档拥有”
python app_so.py
-> http://192.168.1.xxx:5007/stock1
- 如果数据源不同,一切正常,
- 如果数据尚未加载到图中:“RuntimeError:模型只能由一个文档拥有,Selection(id='1043', ...) 已经在文档中”
我读到常见的修复方法是使用不同的源,但我想要共享源,就像我修改的散景服务器示例中那样。
第二次我收到以下警告:在 Flask 中 Js 回调是否必须用于散景?
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>
这是一个 import bokeh
没有外部散景服务器和 vue (vue3,vuex4, composition-api) 的 POC,因为我没有找到满足我需要的教程。
有 2 个散景图通过套索链接 python js_on_change()
通过 python components({})
which generate a js script with Bokeh.embed.embed_items()
inside.
烧瓶
- api 数据
- api Python 散景功能
VueJs
- Vue 3
- vuex 4
- 通过API合成
管理<ol> <li>
列表中的数据反馈和模板视图中的2个散景图
查看 https://github.com/philibe/FlaskVueBokehPOC 了解源代码详细信息。
导入问题
因为discourse.bokeh.org: Node12 import error bokeh 2.0我在frontend/src/pages/ProdSinusPage.vue
.
中通过DOMjavascriptwindow.Bokeh. ...
调用bokehjs
我看过这个 Github 问题 #10658(已打开):[FEATURE] Target ES5/ES6 with BokehJS .
最后我把 <script src="/static/plugins_node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"></script>
放在 frontend/public/index.html
而不是这个脚本 url 和 window.Bokeh. ...
放在 frontend/src/pages/ProdSinusPage.vue
.
链接
- https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
- https://docs.bokeh.org/en/latest/docs/user_guide/embed.html
- 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?
- 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/
代码摘要
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://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,
},
});
前端/
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://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
//
//
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;
TL;DR
我是散景初学者。
我已经在 Whosebug 和 github 中阅读了 https://docs.bokeh.org 或其他示例,但我没有在 Flask 中找到带有 import bokeh
和 2 个没有外部散景服务器的散景图的示例“模型只能由一个文档拥有”
所有示例或教程均适用于散景服务器或 Flask 中嵌入的散景服务器。
09/09/2021:我用 flask、bokeh、vue3、vuex4、composition-api:https://github.com/philibe/FlaskVueBokehPOC 完成了 POC。我清理了我的上一个自动回答并创建了一个带有 POC 作为教程的新自动回答。
问题
我从下面的散景服务器示例开始,由我修改并与共享数据源交互,但我在转换为 Flask 时遇到问题 import bokeh
有 2 个散景图,没有外部散景服务器,而不是“模型只能由一个文档拥有
- https://github.com/bokeh/bokeh/blob/master/examples/app/stocks(在 运行 之前我们必须启动
download_sample_data.py
来获取数据。)
这个问题的预期答案最终是在 Flask 中有一个示例 import bokeh
有 2 个没有外部散景服务器的散景图,而不是“模型必须仅由一个文档拥有"
经过我修改的初始散景服务器示例:有效。
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 服务器源(严重)转换为 Flask import bokeh
带有 2 个散景图,没有外部散景服务器,而不是“模型必须仅由一个文档拥有”
python app_so.py
-> http://192.168.1.xxx:5007/stock1
- 如果数据源不同,一切正常,
- 如果数据尚未加载到图中:“RuntimeError:模型只能由一个文档拥有,Selection(id='1043', ...) 已经在文档中”
我读到常见的修复方法是使用不同的源,但我想要共享源,就像我修改的散景服务器示例中那样。
第二次我收到以下警告:在 Flask 中 Js 回调是否必须用于散景?
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>
这是一个 import bokeh
没有外部散景服务器和 vue (vue3,vuex4, composition-api) 的 POC,因为我没有找到满足我需要的教程。
有 2 个散景图通过套索链接 python js_on_change()
通过 python components({})
which generate a js script with Bokeh.embed.embed_items()
inside.
烧瓶
- api 数据
- api Python 散景功能
VueJs
- Vue 3
- vuex 4
- 通过API合成 管理
<ol> <li>
列表中的数据反馈和模板视图中的2个散景图
查看 https://github.com/philibe/FlaskVueBokehPOC 了解源代码详细信息。
导入问题
因为discourse.bokeh.org: Node12 import error bokeh 2.0我在frontend/src/pages/ProdSinusPage.vue
.
window.Bokeh. ...
调用bokehjs
我看过这个 Github 问题 #10658(已打开):[FEATURE] Target ES5/ES6 with BokehJS .
最后我把 <script src="/static/plugins_node_modules/@bokeh/bokehjs/build/js/bokeh.min.js"></script>
放在 frontend/public/index.html
而不是这个脚本 url 和 window.Bokeh. ...
放在 frontend/src/pages/ProdSinusPage.vue
.
链接
- https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
- https://docs.bokeh.org/en/latest/docs/user_guide/embed.html
- 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?
- 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/
代码摘要
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://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,
},
});
前端/ 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://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
//
//
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;