javascriptplotly-dashoffline

Is there a way to save a dash app to an offline html file, while preserving all interactions (hover, checkbox filtering etc)?


I've tried javascript code, but I'm not familiar with JS and the filtering is not working properly. fig.write_html doesn't work because the checkboxes will not show up.

For context, I have 2 main traces on my graph, go.Scatter and px.timeline. The checkbox will filter which medications show up on the y-axis, and remove/add the scatter points and horizontal bars accordingly.

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))

'''
wrapping text so that it can appear nicely on graph on hover
need to play with width value according to the length of text
'''

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):

    # filtered based on checkbox values
    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")

    # broken barh for ipharm
    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}
    )

    # Add scatter for eimr
    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
                )
            )

    # Add shaded regions for ward duration
    for _, row in ward.iterrows():
        fig.add_vrect(
            x0=row['Adm.Date'],
            x1=row['Disch.Date'],
            fillcolor='grey',
            opacity=0.5,
            line_width=0
            )

    # Add vertical lines for consults
    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)
        )

        # Add scatter points vertically on every vline
        # for hover functionality
        for med in selected_meds:
            fig.add_trace(go.Scatter(
                x = [row['Date']],
                y = [med],
                opacity = 0, # make points invisible
                mode = 'markers',
                marker = dict(size = 5, color = 'black'),
                text = row['summary'],
                hoverinfo = 'text',
                showlegend=False
            ))

    # Update layout
    fig.update_layout(
        xaxis=dict(showgrid=True),
        yaxis=dict(showgrid=True)
    )

    return fig

def save_html():
    # Create the initial figure with all medications

    initial_fig = update_graph(unique_meds)

    # Create JavaScript function for checkbox interactivity
    checkbox_js = """
    <script>
    function updateVisibility() {
        var checkboxes = document.getElementsByClassName('med-checkbox');
        var gd = document.getElementById('timeline-graph');
        var traces = gd.data;
        var selectedMeds = Array.from(checkboxes)
            .filter(cb => cb.checked)
            .map(cb => cb.value);
        var visibility = traces.map(trace => {
            if (trace.y && trace.y.length > 0) {
                return selectedMeds.includes(trace.y[0]) ||
                        selectedMeds.includes(trace.name?.replace('eIMR_', ''));
            }
            return false;
        });
        Plotly.update('timeline-graph',
            {visible: visibility},
            {height: selectedMeds.length * 40},
                
        );
    }

    window.addEventListener('load', function() {
        var gd = document.getElementById('timeline-graph');
        window.originalData = JSON.parse(JSON.stringify(gd.data));
    });
    </script>
    """

   Create HTML string with checkboxes
    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>"

   # Convert figure to HTML
    fig_html = initial_fig.to_html(
        full_html=False,
        include_plotlyjs=True,
        div_id='timeline-graph'
    )

   # Combine everything
    full_html = f"""
    <html>
    <head>
    <title>Medication Timeline</title>
        {checkbox_js}
    </head>
    <body>
        {checkbox_html}
        {fig_html}
    </body>
    </html>
    """
    with open('graph_v1.html', 'w', encoding = 'utf-8') as f:
        f.write(full_html)

if __name__ == "__main__":
    save_html()

Solution

  • The callbacks rely on a flask server backend which means you won't be able to save anything besides the initial state of your app.

    Credit goes to this answer by @Emil on the plotly community form.