【发布时间】:2021-09-14 09:26:37
【问题描述】:
TL;DR
我是散景的初学者。
我已经阅读了 https://docs.bokeh.org 或 stackoverflow 和 github 中的其他示例,但我没有在 Flask 中找到带有 import bokeh 的示例,其中有 2 个没有外部散景服务器的散景图,而不是“模型必须仅由单个文档拥有"
所有示例或教程均适用于 Flask 中嵌入的散景服务器或散景服务器。
09/09/2021:我用烧瓶、散景、vue3、vuex4、composition-api 完成了 POC:https://github.com/philibe/FlaskVueBokehPOC。我清理了我的最后一个自动答案,并使用 POC 作为教程创建了一个新答案。
问题
我从下面的散景服务器示例开始,由我通过与共享数据源的交互进行了修改,但是我在转换为带有 import bokeh 的 Flask 时遇到了问题,其中包含 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 服务器源(严重)转换为带有 import bokeh 的 Flask,带有 2 个 bokeh 图,没有外部 bokeh 服务器,而不是“模型必须仅由单个文档拥有”
python app_so.py -> http://192.168.1.xxx:5007/stock1
- 如果数据源不同,一切正常,
- 如果图中尚未加载数据:“RuntimeError: 模型必须由单个文档拥有,Selection(id='1043', ...) 已在文档中”
我读到常见的解决方法是拥有不同的来源,但我想要共享来源,就像我修改的散景服务器示例一样。
第二次我在下面有这个警告:在 Flask 中,Js 回调对于散景是强制性的吗?
警告:bokeh.embed.util: 您正在生成独立的 HTML/JS 输出,但尝试使用真正的 Python 回调(即使用 on_change 或 on_event)。这种组合是行不通的。
只有 JavaScript 回调可以用于独立输出。更多 有关使用 Bokeh 进行 JavaScript 回调的信息,请参阅:
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>
【问题讨论】:
标签: python flask bokeh vuejs3 vuex4