33  DASH

33.1 重要经验

  • 网页加载时会自动执行,一般情况下不需要执行,应设置 prevent_initial_call = True

  • 官网: https://dash.plotly.com/

33.2 虚拟环境

33.2.1 安装/创建

33.2.1.1 Windows

33.2.1.1.1 查看安装的python版本

没有安装则安装相应版本的python。下载https://www.python.org/downloads/windows/

py -0
33.2.1.1.2 查看默认的python版本
python --version
33.2.1.1.3 创建虚拟环境

以在D:/py3.8文件夹下创建python3.8版本的虚拟环境myenv为例

  • 创建虚拟环境
cd d:/py38

C:\Users\Lenovo\AppData\Local\Programs\Python\Python38\python -m venv myenv

<!-- 最新的python3.10及以后可以直接用 -->

python3.10 -m venv myenv

33.2.2 启动

.\myenv\Scripts\activate

默认情况下,PowerShell禁止运行脚本。因此,当您尝试运行activate脚本时,会收到类似的安全错误。

为此,您可以修改PowerShell的执行策略。请按照以下步骤执行:

  1. 以管理员身份打开PowerShell:在开始菜单中找到PowerShell应用程序,右键单击它,然后选择“以管理员身份运行”。

  2. 运行Get-ExecutionPolicy命令:在PowerShell提示符下,输入以下命令以查看当前的执行策略:

    Get-ExecutionPolicy

    如果结果为“Restricted”,则表示PowerShell禁止运行任何脚本。

  3. 修改执行策略:输入以下命令以将执行策略更改为“RemoteSigned”:

    Set-ExecutionPolicy RemoteSigned

    或者

    Set-ExecutionPolicy Bypass -Scope Process

    执行此命令后,PowerShell将允许运行本地计算机上签名的脚本,但不允许运行来自网络的未签名脚本。

  4. 运行activate脚本:现在,您可以再次尝试运行activate脚本:

    .\myenv\Scripts\activate

    现在,您应该能够成功进入虚拟环境了。

请注意,如果您完成了操作后希望还原原始的执行策略,可以使用以下命令将其设置为Restricted:

Set-ExecutionPolicy Restricted

33.2.3 关闭

deactivate
  • 使用虚拟环境

在d:/py38文件夹下建立工程文件夹。

33.2.4 迁移(以Windows系统为例)

在有网电脑在虚拟环境下建立好项目,再将虚拟环境整体迁移至另外一台无网络电脑以运行项目。

  • 有网络电脑操作
  1. 下载python安装文件并安装,安装时将勾选将安装路径添加至Path。该安装文件到时复制至目标电脑安装,以保持解析器一致。
  2. 新建虚拟环境文件夹,如e:/py312,在该文件夹下创建虚拟环境。
cd e:/py312

C:\Users\Lenovo\AppData\Local\Programs\Python\Python312\python.exe -m venv myvenv
  1. 在myvenv同一目录下创建项目文件夹,如xkjs。一些依赖包都在激活虚拟环境后,在虚拟环境下安装。

  2. 将py312文件夹压缩。

  • 目标电脑(无网络)操作
  1. 将文件夹解压至某目录下,最好是相同目录下。
  2. 用同一个python安装包,安装python。
  3. 在powershell启动虚拟环境,在虚拟环境下进行相关操作。
注意

两台电脑因用户名不同,python的安装路径往往不同,直接启动虚拟环境会报错。这时只要将虚拟环境文件夹myvenv下的配置文件pyvenv.cfg的配置改为实际目标电脑的情况即可。

home = C:\Users\Lenovo\AppData\Local\Programs\Python\Python312
include-system-site-packages = false
version = 3.12.1
executable = C:\Users\Lenovo\AppData\Local\Programs\Python\Python312\python.exe
command = C:\Users\Lenovo\AppData\Local\Programs\Python\Python312\python.exe -m venv E:\py312\myvenv

33.3 启动app

33.3.1 Windows

  • 项目文件夹跟虚拟环境venv文件夹应放在同一个目录
Code
display_hierarchy <- function(path, indent = 0) {
  items <- list.files(path, full.names = TRUE)
  
  for (item in items) {
    cat(rep("  ", indent))
    
    if (isTRUE(file.info(item)$isdir)) {
      cat("📁 ", basename(item), "\n")
      display_hierarchy(item, indent + 1)
    } else {
      cat("📄 ", basename(item), "\n")
    }
  }
}

display_hierarchy('data/demo-display-hierarchy/')
📁  venv 
📁  xkjs 
  📄  app.py 

在终端输入以下内容启动虚拟环境和app

cd e:/py310
.\venv\Scripts\activate
cd xkjs
python app.py

33.4 server设置

33.4.1 设置 static 文件夹的路径

app.server.static_folder = ‘static’

33.5 架构

33.6 加载外部资源

33.6.1 成功实践

33.6.1.1 particleground

GitHub链接

  1. 加载css或js文件(放在assets的css或js文件下,其实任意命名的文件夹都可)。
📄  app.py 
📁  assets 
  📁  css 
     📄  style.css 
  📁  js 
     📄  jquery.particleground.js 

或者直接放在assets文件下,dash会自动加载:

📄  app.py 
📁  assets 
  📄  jquery.particleground.js 
  📄  style.css 
  1. 网页启动时启动函数
  • 在静态html文件里,通过以下head部分引用的demo.js文件启动。
demo.js文件内容
document.addEventListener('DOMContentLoaded', function () {
  particleground(document.getElementById('particles'), {
    dotColor: '#5cbdaa',
    lineColor: '#5cbdaa'
  });
  var intro = document.getElementById('intro');
  intro.style.marginTop = - intro.offsetHeight / 2 + 'px';
}, false);

/* 或者
// jQuery plugin example:
$(document).ready(function() {
  $('#particles').particleground({
    dotColor: '#5cbdaa',
    lineColor: '#5cbdaa'
  });
  $('.intro').css({
    'margin-top': -($('.intro').height() / 2)
  });
});
*/
  • 而在dash里需要通过客户端回调,或者feffery-util-components回调。feffery-util-components由以下部分组成。
app.layout部分
dcc.Location(id='url'),
fuc.FefferyExecuteJs(id='execute-js'),
# 背景粒子动画挂载点
html.Div(
    id='particles-mount',
    style={
        'position': 'fixed',
        'top': 0,
        'left': 0,
        'right': 0,
        'bottom': 0,
        'zIndex': -1
    },
)
函数部分

这里的函数相当于静态网页的

# Function to generate particle effect JavaScript string
def generate_particle_effect():
  return '''
    particleground(document.getElementById('particles-mount'), {
    dotColor: '#5cbdaa',
    lineColor: '#5cbdaa'
    });
  '''
回调函数部分

在网页加载时激活回调

# Callback for particle effect
@app.callback(
    Output('execute-js', 'jsString'),
    Input('url', 'pathname')
)
def render_js(pathname):
    print(pathname)
    if pathname in ["/login", "/relogin"]:
        return generate_particle_effect()
    else:
        return dash.no_update

33.6.2 实践提升

  • 不需要style.css文件,因其影响登录后的页面的css和javascript.
  • 改造jquery.particleground.js,增加backgroundColor属性。改造的内容主要是以下2处:
window[pluginName].defaults = {
    backgroundColor: '#16a085', // 设置背景颜色
}


function styleCanvas() {
    canvas.style.backgroundColor = options.backgroundColor; // 设置背景颜色
}

在app.py回调时里可以直接设置背景色参数

# Function to generate particle effect JavaScript string
def generate_particle_effect():
  return '''
    particleground(document.getElementById('particles-mount'), {
    dotColor: '#5cbdaa',
    lineColor: '#5cbdaa',
    backgroundColor: '#16a085'
    });
  '''

33.6.3 基于JavaScript的电子动画项目推荐

JavaScript的电子动画项目
  1. particles.js:一个轻量级的粒子动画库,可以创建各种粒子效果的背景动画。GitHub链接

  2. Particleground:一个基于 jQuery 的粒子背景效果库,可以为登录界面添加交互式的粒子效果。GitHub链接

  3. TSParticles:一个功能强大的粒子背景效果库,支持各种粒子效果和交互式特性。它基于 TypeScript 和 Webpack,并且可以与 Angular、React 等框架配合使用。GitHub链接

  4. Vanta.js:一个使用 WebGL 创建视觉效果的库,可以为登录界面添加各种背景效果,包括粒子、云雾、流体等效果。GitHub链接

  5. Canvas Particles:一个使用 HTML5 Canvas 创建粒子效果的库,可以为登录界面添加动态的粒子背景效果。GitHub链接

这些库都提供了丰富的选项和配置,可以帮助你实现类似 particles.js 的登录界面背景效果。你可以根据自己的需求和喜好选择合适的库来实现粒子背景效果。

33.7 回调

33.7.1 正则匹配id回调

还可以参考https://dash.plotly.com/flexible-callback-signatures可AI提问

from dash import Dash, callback, Input, Output

app = Dash(__name__)

# Assume you have input and output ids like 'upload_xxxx' and 'upload_table_xxxx'
input_ids = [f'upload_{random_string}' for random_string in random_strings]
output_ids = [f'upload_table_{random_string}' for random_string in random_strings]

@app.callback(
    [Output(output_id, 'data') for output_id in output_ids],
    [Input(input_id, 'data') for input_id in input_ids]
)
def update_tables(*args):
    # Your callback logic here
    pass

if __name__ == '__main__':
    app.run_server(debug=True)

33.7.2 ALL回调

多对一回调(多个Input回调至一个Output)

# ALL使用

from dash import Dash, dcc, html, Input, Output, ALL, Patch, callback

app = Dash(__name__)

app.layout = html.Div(
    [
        html.Button("Add Filter", id="add-filter-btn", n_clicks=0),
        html.Div(id="dropdown-container-div", children=[]),
        html.Div(id="dropdown-container-output-div"),
    ]
)


@callback(
    Output("dropdown-container-div", "children"), Input("add-filter-btn", "n_clicks")
)
def display_dropdowns(n_clicks):
    patched_children = Patch()
    new_dropdown = dcc.Dropdown(
        ["NYC", "MTL", "LA", "TOKYO"],
        id={"type": "city-filter-dropdown", "index": n_clicks},
    )
    patched_children.append(new_dropdown)
    return patched_children


@callback(
    Output("dropdown-container-output-div", "children"),
    Input({"type": "city-filter-dropdown", "index": ALL}, "value"),
)
def display_output(values):
    return html.Div(
        [html.Div(f"Dropdown {i + 1} = {value}") for (i, value) in enumerate(values)]
    )


if __name__ == "__main__":
    app.run(debug=True)

总结:MATCH使用时,Input, State, Ouput必须也是MATCH,ALL使用时,Output可以是一个。

33.7.3 MATCH回调

一对一回调,Input的值回调至相同index的Ouput。 MATCH的含义指整个callback装饰器的各个组件id的index部分的MATCH的值必须相同。

# MATCH举例

from dash import Dash, dcc, html, Input, Output, State, MATCH, Patch, callback

app = Dash(__name__)

app.layout = html.Div([
    html.Button("Add Filter", id="dynamic-add-filter-btn", n_clicks=0),
    html.Div(id='dynamic-dropdown-container-div', children=[]),
])

@callback(
    Output('dynamic-dropdown-container-div', 'children'),
    Input('dynamic-add-filter-btn', 'n_clicks')
    )
def display_dropdowns(n_clicks):
    patched_children = Patch()

    new_element = html.Div([
        dcc.Dropdown(
            ['NYC', 'MTL', 'LA', 'TOKYO'],
            id={
                'type': 'city-dynamic-dropdown',
                'index': n_clicks
            }
        ),
        html.Div(
            id={
                'type': 'city-dynamic-output',
                'index': n_clicks
            }
        )
    ])
    patched_children.append(new_element)
    return patched_children


@callback(
    Output({'type': 'city-dynamic-output', 'index': MATCH}, 'children'),
    Input({'type': 'city-dynamic-dropdown', 'index': MATCH}, 'value'),
    State({'type': 'city-dynamic-dropdown', 'index': MATCH}, 'id'),
)
def display_output(value, id):
    return html.Div(f"Dropdown {id['index']} = {value}")


if __name__ == '__main__':
    app.run(debug=True)

33.7.4 回调判断

  • ctx和ctxs同时判断更加精准:在涉及链式回调(组件作为Output又同时作为Input)式比较有用。实例:ICU质控数据填报系统导入数据时,第1个数据被清空。@有些版本有这个问题,有些版本没有。

33.7.5 慎用公有变量

在Flask中,一旦用户登录,你可以随时更新session中的信息,包括用户名等数据。session会在用户会话期间一直保持,直到会话结束(例如用户登出,或者session过期)。以下是一个简单的示例,展示了如何在用户登录时设置session,并在之后更新用户名信息:

首先,确保你已经在Flask应用中启用了session。这通常通过设置一个secret_key完成:

from flask import Flask, session, request, redirect, url_for

app = Flask(__name__)
app.secret_key = 'your_secret_key_here'  # 应替换为一个随机的、复杂的密钥

接下来,假设有一个登录路由,用户提交登录表单后,你可以设置session:

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    
    # 这里应该有验证用户名和密码的逻辑,这里省略
    
    if is_valid_login(username, password):  # 假设这是验证登录的函数
        session['logged_in'] = True
        session['username'] = username  # 设置用户名到session中
        return redirect(url_for('dashboard'))  # 登录成功后重定向到仪表板或其他页面
    else:
        return 'Invalid credentials'

如果你想在用户已经登录的情况下更新session中的用户名(比如用户修改了他们的用户名),你可以在相应的路由中更新session:

@app.route('/update_username', methods=['POST'])
def update_username():
    new_username = request.form.get('new_username')
    
    # 这里应该有验证新用户名是否有效的逻辑,以及用户是否有权限更改用户名的检查,这里省略
    
    if session.get('logged_in'):  # 检查用户是否已登录
        session['username'] = new_username  # 更新用户名
        return 'Username updated successfully'
    else:
        return redirect(url_for('login'))  # 如果未登录,重定向到登录页面
        

当用户登出时,你可以清除session中的所有信息,包括用户名:

@app.route('/logout')
def logout():
    # 清除session数据
    session.pop('logged_in', None)
    session.pop('username', None)
    return redirect(url_for('login'))  # 重定向到登录页面

这样,你就能够根据用户的行为动态地更新session中的信息,同时确保在用户登出后,相关的session数据被清理。

33.8 项目代码导出

方便查询与修改

exportcode2txt.py
  • 放在根目录下
import os
def write_py_files_content_to_file(directory, output_file):
    with open(output_file, 'w', encoding='utf-8') as out_f:
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith(".py") and 'checkpoint' not in file:
                    file_path = os.path.join(root, file)
                    out_f.write(f"File: {file_path}\n")
                    with open(file_path, 'r', encoding='utf-8') as in_f:
                        lines = in_f.readlines()
                        for line in lines:
                            out_f.write(f"{file_path}: {line}")  # 将文件内容及文件路径写入到文本文档中
                    out_f.write("\n\n")


if __name__ == '__main__':
    write_py_files_content_to_file('.','code.txt')

通过在终端执行以下代码将代码导出

python exportcode2txt.py

33.9 组件

33.9.1 fac.AntdAnchor

  • 生成linkdict
import json
import re

def generate_links_dict(markdown_content, match_pattern):
    '''
    match_pattern:用捕获组获取#号,id和title
    '''
    links = []
    stack = []

    for line in markdown_content.split('\n'):
        match = re.match(match_pattern, line)
        if match:
            level = len(match.group(1))
            id = match.group(2)
            title = match.group(3).strip()
            href = f'#{id}'
            link = {'title': title, 'href': href, 'children': []}

            while stack and stack[-1]['level'] >= level:
                stack.pop()

            if stack:
                stack[-1]['item']['children'].append(link)
            else:
                links.append(link)

            stack.append({'level': level, 'item': link})
    link_dict = json.dumps(links, indent=2, ensure_ascii=False)
    return link_dict



markdownstr='''
# major-complaint 主诉 
# presenting-history 现病史
# past-history 既往史
# system-review 系统回顾
# personal-history 个人史
# wedding-feeding-history 婚育史
# family-history 家族史
# vital-sign 生命征
# general-status 一般情况
# skin-mucous 皮肤粘膜
# superficial-lymph-nodes 浅表淋巴结
# skull 头颅
## eyes 眼
## ears 耳
## nose 鼻
## mouth 口
# neck 颈部
# chest-examination 胸部
## chest-visual-examination 胸部视诊
## chest-palpation 胸部触诊
## chest-percussion 胸部叩诊
## chest-auscultation 胸部听诊
# heart-examination 心脏
## heart-visual-examination 心脏视诊
## heart-palpation 心脏触诊
## heart-percussion 心脏叩诊
## heart-auscultation 心脏听诊
'''
match_pattern = r'^(#+)\s+([a-z-]+)\s*(.*)'
link_dict = generate_links_dict(markdownstr, match_pattern)

print(link_dict)

33.9.2 一键组件及回调

从Excel文件生成组件,并且生成回调函数和数据库,实现快速部署数据的前后端交互。

33.10 部署

33.10.1 Linux-Ubuntu

33.10.1.1 corpus

screen -S corpus
source dash/bin/activate
cd dash/app/corpus

<!-- waitress-serve --port=8052 app:app.server -->
<!-- app指的是接入文件,改为实际的名称,如 -->

waitress-serve --port=8052 home:app.server

输入http://www.mmphcrc.com:8052/corpus/query/可访问。下次通过screen -r corpus进入相应终端页面,按ctrl+c终止服务器,输入waitress-serve --port=8052 home:app.server重新启动服务器。

33.10.1.2 timeline

screen -S timeline
source dash/bin/activate
cd dash/app/时间线

waitress-serve --port=8050 wsgi:app.server

33.10.1.3 结构化病历模板

screen -S jgh
cd myapp
source myvenv/bin/activate
cd jghblmb
waitress-serve --port=8052 app:app.server

33.10.1.4 screen相关操作

screen -ls  # 查看screen列表
screen -r corpus #进入screen corpus
screen -d corpus #退出
screen -X -S corpus quit # 删除screen

33.10.2 Windows

Windows部署dash应用的方法(不会有powershell弹窗):

  • 新建启动powershell脚本至ps1文件。

cd E:\jupyterlab\py310/icudatafill
waitress-serve --port=8055 app:app.server
  • 设置定时任务 启动计划任务
taskschd.msc
  • 任务设置:
    • 常规选项卡:1. 不管用户是否登录都要运行(不储存密码)2. 最高权限运行。

    • 操作选项卡:程序或脚本:powershell;添加参数:-ExecutionPolicy Bypass -File E:\jupyterlab\scripts\deploy-icudatafill.ps1

    • 设置选项卡:取消勾选如果任务运行超过以下时间,则停止运行。