You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将带交互功能的Dash应用保存为离线HTML文件?

将Dash应用保存为带完整交互的离线HTML文件解决方案

问题核心

原方案的JavaScript逻辑错误在于:Plotly Express生成的px.timeline是单个迹线包含所有药物的条形,而非每个药物对应一个迹线;同时没有正确处理咨询点的隐藏逻辑,导致过滤功能失效。

修正后的完整代码

import textwrap
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from flask import Flask
from dash import Dash, html, dcc, Input, Output

# 假设你的数据变量(ipharm, eimr, ward, notes)已提前定义

server = Flask(__name__)
app = Dash(__name__, server=server)

ipharm_meds = list(ipharm['meds'].unique())
eimr_meds = list(eimr['meds'].unique())
unique_meds = list(set(ipharm_meds + eimr_meds))

# 处理悬停文本换行
wrapper = textwrap.TextWrapper(width=100)
notes['summary'] = notes['summary'].apply(lambda x: wrapper.fill(x))
notes['summary'] = notes['summary'].apply(lambda x: x.replace('\n', '<br>'))

app.layout = html.Div([
    html.Div([
        dcc.Checklist(
            id='med-checklist',
            options=[{'label': med, 'value': med} for med in unique_meds],
            value=unique_meds,
            inline=True
        ),
    ], style={'width': '100%', 'padding': '10px'}),
    dcc.Graph(id='timeline-graph')
])

@app.callback(
    Output('timeline-graph', 'figure'),
    Input('med-checklist', 'value')
)
def update_graph(selected_meds):
    # 过滤药物数据
    ipharm_filtered = ipharm[ipharm['meds'].isin(selected_meds)]
    ipharm_filtered['Start'] = pd.to_datetime(ipharm_filtered['Start'])
    ipharm_filtered['End'] = pd.to_datetime(ipharm_filtered['End'])
    ipharm_filtered['Start_str'] = ipharm_filtered['Start'].dt.strftime("%d-%m-%Y")
    ipharm_filtered['End_str'] = ipharm_filtered['End'].dt.strftime("%d-%m-%Y")

    # 生成timeline图
    fig = px.timeline(
        ipharm_filtered,
        x_start='Start',
        x_end='End',
        y='meds',
        hover_data={'Start': False,
                   'End': False,
                   'Start_str': True,
                   'End_str': True,
                   'FREQUENCY': True,
                   'DOSAGE INSTRUCTION': True,
                   'dose': True}
    )

    # 添加eIMR散点迹线,每个药物对应一个迹线,方便后续JS识别
    for med in selected_meds:
        med_data = eimr[eimr['meds'] == med]
        if not med_data.empty:
            fig.add_trace(go.Scatter(
                x=med_data['ServingDateTime'],
                y=[med] * len(med_data),
                mode='markers',
                marker=dict(size=5, color='red'),
                name=f'eIMR_{med}',
                text=med_data['Dose'],
                hoverinfo='text',
                showlegend=False,
                customdata=[med] * len(med_data)  # 自定义字段存储药物名称
            ))

    # 添加病房时长阴影区域
    for _, row in ward.iterrows():
        fig.add_vrect(
            x0=row['Adm.Date'],
            x1=row['Disch.Date'],
            fillcolor='grey',
            opacity=0.5,
            line_width=0
        )

    # 添加咨询垂直线条及悬停点,每个药物对应一个迹线
    for _, row in notes.iterrows():
        fig.add_shape(
            type='line',
            x0=row['Date'],
            x1=row['Date'],
            y0=0,
            y1=1,
            yref='paper',
            line=dict(color='black', width=1)
        )

        for med in selected_meds:
            fig.add_trace(go.Scatter(
                x=[row['Date']],
                y=[med],
                opacity=0,
                mode='markers',
                marker=dict(size=5, color='black'),
                text=row['summary'],
                hoverinfo='text',
                showlegend=False,
                customdata=[med]  # 自定义字段存储药物名称
            ))

    # 更新布局
    fig.update_layout(
        xaxis=dict(showgrid=True),
        yaxis=dict(showgrid=True)
    )

    return fig

def save_html():
    initial_fig = update_graph(unique_meds)

    # 修正后的JavaScript交互逻辑
    checkbox_js = """
    <script>
    function updateVisibility() {
        const checkboxes = document.getElementsByClassName('med-checkbox');
        const gd = document.getElementById('timeline-graph');
        const selectedMeds = Array.from(checkboxes)
            .filter(cb => cb.checked)
            .map(cb => cb.value);
        
        // 处理timeline主迹线:过滤y轴数据
        const timelineTrace = gd.data[0];
        const visibleIndices = timelineTrace.y.map(y => selectedMeds.includes(y));
        const filteredXStart = timelineTrace.x_start.filter((_, idx) => visibleIndices[idx]);
        const filteredXEnd = timelineTrace.x_end.filter((_, idx) => visibleIndices[idx]);
        const filteredY = timelineTrace.y.filter((_, idx) => visibleIndices[idx]);
        const filteredCustomdata = timelineTrace.customdata ? timelineTrace.customdata.filter((_, idx) => visibleIndices[idx]) : [];

        // 处理其他迹线(eIMR散点、咨询悬停点)
        const otherTracesVisibility = gd.data.slice(1).map(trace => {
            // 通过customdata或name判断所属药物
            const med = trace.customdata?.[0] || trace.name?.replace('eIMR_', '');
            return selectedMeds.includes(med);
        });

        // 合并更新数据
        const updatedData = [
            {
                x_start: filteredXStart,
                x_end: filteredXEnd,
                y: filteredY,
                customdata: filteredCustomdata,
                visible: filteredY.length > 0
            },
            ...gd.data.slice(1).map((trace, idx) => ({
                visible: otherTracesVisibility[idx]
            }))
        ];

        // 更新Y轴类别为选中的药物
        const updatedLayout = {
            yaxis: {
                categoryorder: 'array',
                categoryarray: selectedMeds
            },
            height: Math.max(400, selectedMeds.length * 40)
        };

        Plotly.update('timeline-graph', updatedData, updatedLayout);
    }

    // 页面加载时保存原始数据,用于重置
    window.addEventListener('load', function() {
        const gd = document.getElementById('timeline-graph');
        window.OriginalTimelineData = JSON.parse(JSON.stringify(gd.data[0]));
        window.OriginalOtherTraces = JSON.parse(JSON.stringify(gd.data.slice(1)));
    });
    </script>
    """

    # 生成复选框HTML
    checkbox_html = '<div style="width:100%; padding:10px">'
    for med in unique_meds:
        checkbox_html += f'''
        <label style="margin-right:10px">
            <input type="checkbox" class="med-checkbox" value="{med}" checked onclick="updateVisibility()">
            {med}
        </label>
        '''
    checkbox_html += '</div>'

    # 转换图表为HTML
    fig_html = initial_fig.to_html(
        full_html=False,
        include_plotlyjs='cdn',  # 使用CDN减小文件体积,也可改为True嵌入本地JS
        div_id='timeline-graph'
    )

    # 组合完整HTML
    full_html = f"""
    <html>
    <head>
        <title>Medication Timeline</title>
        {checkbox_js}
    </head>
    <body>
        {checkbox_html}
        {fig_html}
    </body>
    </html>
    """
    with open('medication_timeline.html', 'w', encoding='utf-8') as f:
        f.write(full_html)

if __name__ == "__main__":
    save_html()

关键修正说明

  • Timeline迹线处理:原px.timeline生成的单个迹线包含所有药物数据,JS中通过过滤迹线的x_start、x_end、y数组,只保留选中药物的数据
  • 迹线标识优化:为eIMR散点和咨询悬停点添加customdata字段存储药物名称,让JS能准确识别迹线所属药物
  • Y轴动态更新:过滤后同步更新Y轴的类别数组,确保只显示选中的药物
  • 布局自适应:根据选中药物数量动态调整图表高度,避免内容溢出

内容的提问来源于stack exchange,提问作者Noel Teo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 23:47:03