如何将带交互功能的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
相关产品推荐
相关产品推荐

