【问题标题】:Flask with `import bokeh` with 2 bokeh graphs without external bokeh server and not "Models must be owned by only a single document"带有 `import bokeh` 的烧瓶,带有 2 个没有外部散景服务器的散景图,而不是“模型只能由一个文档拥有”
【发布时间】: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 个没有外部散景服务器的散景图,而不是“必须拥有模型”仅通过一个文档”

这个问题的预期答案最终是在 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


    【解决方案1】:

    这是一个带有 import bokeh 的 POC,没有外部散景服务器和 vue (vue3,vuex4, composition-api),因为我没有找到适合我需要的教程。

    有 2 个散景图通过套索链接到 python js_on_change() 通过python components({}) which generate a js script with Bokeh.embed.embed_items() inside

    • 烧瓶

      • api 数据
      • api Python Bokeh 函数
    • VueJs

      • Vue 3
      • vuex 4
      • 通过 API 组合管理 &lt;ol&gt; &lt;li&gt; 列表中的数据反馈和模板视图中的 2 个散景图

    查看https://github.com/philibe/FlaskVueBokehPOC 了解源代码详情。

    导入问题

    由于discourse.bokeh.org: Node12 import error bokeh 2.0,我在frontend/src/pages/ProdSinusPage.vue 中通过DOM javascript window.Bokeh. ... 调用bokehjs。

    我看过这个 Github 问题 #10658(已打开):[FEATURE] Target ES5/ES6 with BokehJS

    链接

    代码摘要

    server/config.py

    SECRET_KEY = 'GITHUB6202f13e27c5'
    PORT_FLASK_DEV = 8071
    PORT_FLASK_PROD = 8070
    PORT_NODE_DEV = 8072
    

    server/app.py

    from flask import (
        Flask,
        jsonify,
        request,
        render_template,
        flash,
        redirect,
        url_for,
        session,
        send_from_directory,
        # abort,
    )
    
    from bokeh.layouts import row, column, gridplot, widgetbox
    
    from flask_cors import CORS
    import uuid
    import os
    
    
    from bokeh.embed import json_item, components
    from bokeh.plotting import figure, curdoc
    from bokeh.models.sources import AjaxDataSource, ColumnDataSource
    
    
    from bokeh.models import CustomJS
    
    # from bokeh.models.widgets import Div
    
    bokeh_tool_tips = [
        ("index", "$index"),
        ("(x,y)", "($x, $y)"),
        # ("desc", "@desc"),
    ]
    
    bokeh_tool_list = ['pan,wheel_zoom,lasso_select,reset']
    
    import math
    import json
    
    
    from flask_debugtoolbar import DebugToolbarExtension
    
    from werkzeug.utils import import_string
    
    from werkzeug.serving import run_simple
    from werkzeug.middleware.dispatcher import DispatcherMiddleware
    
    
    def create_app(PROD, DEBUG):
    
        app = Flask(__name__)
    
        app.dir_app = os.path.abspath(os.path.dirname(__file__))
        app.app_dir_root = os.path.dirname(app.dir_app)
        app.app_dir_nom = os.path.basename(app.dir_app)
    
        print(app.dir_app)
        print(app.app_dir_root)
        print(app.app_dir_nom)
    
        if not PROD:
            CORS(app, resources={r'/*': {'origins': '*'}})
            template_folder = '../frontend/public'
            static_url_path = 'static'
            static_folder = '../frontend/public/static'
    
        else:
            template_folder = '../frontend/dist/'
            static_url_path = 'static'
            static_folder = '../frontend/dist/static'
    
        app.template_folder = template_folder
        app.static_url_path = static_url_path
        app.static_folder = static_folder
    
        # à rajouter
        # app.wsgi_app = ReverseProxied(app.wsgi_app, script_name='/' + app.app_dir_nom)
    
        app.debug = DEBUG
    
        app.config.from_pyfile('config.py')
        if DEBUG:
            toolbar = DebugToolbarExtension()
            toolbar.init_app(app)
    
        @app.before_first_request
        def initialize():
            session.clear()
            if not session.get('x'):
                session['x'] = 0
            if not session.get('y'):
                session['y'] = 0
            if not session.get('HistoryArray'):
                session['HistoryArray'] = [{'x': None, 'y': None}]
    
        @app.route('/')
        def index():
            VariableFlask = 'VariableFlaskRendered'
            return render_template('index.html', VariableFlask=VariableFlask)
    
        @app.route('/favicon.ico')
        def favicon():
            return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/x-icon')
    
        @app.route('/static/plugins_node_modules/<path:path>')
        def send_plugins_(path):
            print(app.app_dir_root)
            print(os.path.join(app.app_dir_root, 'frontend', 'node_modules'))
            return send_from_directory((os.path.join(app.app_dir_root, 'frontend', 'node_modules')), path)
    
        # https://stackoverflow.com/questions/37083998/flask-bokeh-ajaxdatasource
        # https://github.com/bokeh/bokeh/blob/main/examples/embed/json_item.py
    
        @app.route("/api/datasinus/<operation>", methods=['GET', 'POST'])
        def get_x(operation):
            if not session.get('x'):
                session['x'] = 0
            if not session.get('y'):
                session['y'] = 0
            if not session.get('HistoryArray'):
                session['HistoryArray'] = [{'x': None, 'y': None}]
    
            # global x, y
            if operation == 'increment':
                session['x'] = session['x'] + 0.1
    
            session['y'] = math.sin(session['x'])
    
            if operation == 'increment':
                session['HistoryArray'].append({'x': session['x'], 'y': session['y']})
                return jsonify(x=[session['x']], y=[session['y']])
            else:
                response_object = {'status': 'success'}
                # malist[-10:] last n elements
                # malist[::-1] reversing using list slicing
                session['HistoryArray'] = session['HistoryArray'][-10:]
                response_object['sinus'] = session['HistoryArray'][::-1]
                return jsonify(response_object)
    
        @app.route("/api/bokehinlinejs", methods=['GET', 'POST'])
        def simple():
            streaming = True
    
            s1 = AjaxDataSource(data_url="/api/datasinus/increment", polling_interval=1000, mode='append')
    
            s1.data = dict(x=[], y=[])
    
            s2 = ColumnDataSource(data=dict(x=[], y=[]))
    
            s1.selected.js_on_change(
                'indices',
                CustomJS(
                    args=dict(s1=s1, s2=s2),
                    code="""
                var inds = cb_obj.indices;
                var d1 = s1.data;
                var d2 = s2.data;
                d2['x'] = []
                d2['y'] = []
                for (var i = 0; i < inds.length; i++) {
                    d2['x'].push(d1['x'][inds[i]])
                    d2['y'].push(d1['y'][inds[i]])
                }
                s2.change.emit();
                
                """,
                ),
            )
    
            p1 = figure(
                x_range=(0, 10),
                y_range=(-1, 1),
                plot_width=400,
                plot_height=400,
                title="Streaming, take lasso to copy points (refresh after)",
                tools=bokeh_tool_list,
                tooltips=bokeh_tool_tips,
                name="p1",
            )
            p1.line('x', 'y', source=s1, color="blue", selection_color="green")
            p1.circle('x', 'y', size=1, source=s1, color=None, selection_color="red")
    
            p2 = figure(
                x_range=p1.x_range,
                y_range=(-1, 1),
                plot_width=400,
                plot_height=400,
                tools=bokeh_tool_list,
                title="Watch here catched points",
                tooltips=bokeh_tool_tips,
                name="p2",
            )
            p2.circle('x', 'y', source=s2, alpha=0.6)
    
            response_object = {}
            response_object['gr'] = {}
    
            script, div = components({'p1': p1, 'p2': p2}, wrap_script=False)
            response_object['gr']['script'] = script
            response_object['gr']['div'] = div
            return response_object
    
        return app
    
    
    if __name__ == '__main__':
        from argparse import ArgumentParser
    
        parser = ArgumentParser()
        parser.add_argument('--PROD', action='store_true')
        parser.add_argument('--DEBUG', action='store_true')
        args = parser.parse_args()
    
        DEBUG = args.DEBUG
        PROD = args.PROD
    
        print('DEBUG=', DEBUG)
        print('PROD=', PROD)
    
        app = create_app(PROD=PROD, DEBUG=DEBUG)
    
        if not PROD:
            PORT = app.config["PORT_FLASK_DEV"]
        else:
            PORT = app.config["PORT_FLASK_PROD"]
    
        if DEBUG:
            app.run(host='0.0.0.0', port=PORT, debug=DEBUG)
    
        else:
            from waitress import serve
    
            serve(app, host="0.0.0.0", port=PORT)
    
    

    前端/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>
    
    

    前端/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)
            )
          );
      },
    };
    

    前端/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;
    

    前端/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",
    });
    

    前端/vue.config.js

    // https://stackoverflow.com/questions/50828904/using-environment-variables-with-vue-js/57295959#57295959
    // https://www.fatalerrors.org/a/vue3-explains-the-configuration-of-eslint-step-by-step.html
    
    const webpack = require("webpack");
    
    const env = process.env.NODE_ENV || "dev";
    
    const path = require("path");
    
    module.exports = {
      indexPath: "index.html",
      assetsDir: "static/app/",
    
      configureWebpack: {
        resolve: {
          extensions: [".js", ".vue", ".json", ".scss"],
          alias: {
            styles: path.resolve(__dirname, "src/assets/scss"),
          },
        },
        plugins: [
          new webpack.DefinePlugin({
            // allow access to process.env from within the vue app
            "process.env": {
              NODE_ENV: JSON.stringify(env),
              CONFIG_PORTFLASK: JSON.stringify(process.env.CONFIG_PORTFLASK),
              CONFIG_PORTNODEDEV: JSON.stringify(process.env.CONFIG_PORTNODEDEV),
            },
          }),
        ],
      },
    
      devServer: {
        watchOptions: {
          poll: true,
        },
        proxy: {
          "/api": {
            target: "http://localhost:" + process.env.CONFIG_PORTFLASK + "/",
            changeOrigin: true,
            pathRewrite: {
              "^/api": "/api",
            },
          },
    
          "/static/plugins_node_modules": {
            target: "http://localhost:" + process.env.CONFIG_PORTFLASK + "/",
            changeOrigin: true,
            pathRewrite: {
              "^/static/plugins_node_modules": "/static/plugins_node_modules/",
            },
          },
        },
      },
    
      chainWebpack: (config) => {
        config.module
          .rule("vue")
          .use("vue-loader")
          .loader("vue-loader")
          .tap((options) => {
            options.compilerOptions = {
              delimiters: ["[[", "]]"],
            };
            return options;
          });
      },
    
      lintOnSave: true,
    };
    
    // https://prettier.io/docs/en/install.html
    // https://www.freecodecamp.org/news/dont-just-lint-your-code-fix-it-with-prettier/
    
    

    前端/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;
    

    【讨论】:

    • 我已经用session$registerDataObj()(没有 rbokeh 和 shiny.router)在纯 R Shiny 中使用 R Shiny 和散景完成了 POC:):github.com/philibe/RshinyBokehReticulatePOC
    • 嗨@phili_b,恭喜。我很高兴你能找到解决方案。我将尝试使用 fastapi 和 vuejs 来实现它。
    • 谢谢@William。我很好奇你未来的实施。我去看看。
    猜你喜欢
    • 2017-02-05
    • 2014-10-16
    • 1970-01-01
    • 2016-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多