'Download Plotly Figure as HTML object using Dash Callback in python

I am trying to download a series of plots using a button. I currently have one button that creates the graphs and displays it on the dashboard. The other button should download all the graphs as html files.

Here is my code so far. It will write the plot to an html file in the same file path but it will not download it. I need the download piece so that when I publish it, it is not restricted by the filepath and instead downloads to the downloads folder.

import dash
import plotly.graph_objects as go
from dash import Dash, dcc, html, Input, Output

app = Dash(__name__)
app.layout = html.Div([
    html.Button("Submit", id="submit-val"),
    html.Button("Download Plots", id="btn-downl-plts-html"),
    dcc.Download(id="btn-dwnld")
])

@app.callback(
    [Output("fig-list", "value")],
    [Input("submit-val", "n_clicks")]
)
def graphing(n_clicks):
    if n_clicks != None:
       ...function to create plots
       fig_list = [fig1,fig2,fig3]
    return fig_list

@app.callback(
    [Output("btn-dwnld", "data")],
    [Input("btn-downl-plts-html", "n_clicks")],
    [State("fig-list", "value")],
)
def dwnld_btn(n_clicks, fig_list):
    if n_clicks != None:
        for fig in fig_list:
            # figs pull in as dicts for some reason so turn back to figure
            fig = go.Figure(fig)
            # use the title of the graph as the figure download path
            s = str(fig.full_figure_for_development)
            #the plot titles are nestled in bold tags
            start = s.find("<b>") + len("<b>")
            end = s.find("</b>")
            substring = s[start:end]
            filename = f"{substring}.html"
            fig.write_html(filename)
            print(filename)
            return dcc.send_file(filename)

I am currently getting a few errors: dash._grouping.SchemaTypeValidationError: Schema: [<Output btn-dwnld.data>] Path: () Expected type: (<class 'tuple'>, <class 'list'>) Received value of type <class 'dict'>: {'content': 'PGh0bWw+DQo8aGVhZD48bWV0YSBjaGFyc2V0PSJ1dGYtOCIgLz48L2hlYWQ+DQo8Ym9keT4NCiAgICA8ZGl2PiAgICAgICAgICAgICAgICAgICAgICAgIDxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij53aW5kb3cuUGxvdGx5Q29uZmlnID0ge01hdGhKYXhDb25maWc6ICdsb2NhbCd9Ozwvc2NyaXB0Pg0KICAgICAgICA8c2NyaXB ......................} and then it spits out the dictionary representation of the plot but in a weird coded way.

I would appreciate any help and optimization on this issue.



Solution 1:[1]

I found somewhat of a roundabout solution but it works. If anyone has a better solution I am all ears.

The explanation here is that I pass a list of plots from one callback that builds them into another that activates on a button click. I grab the title of the first plot to use as the file name. I then write multiple figures to one HTML by appending DIVs containing figures and saving it to a folder using the filename described above. I then use dcc.send_file grabbing that file path to dcc.Download to download the file. I then pass that file path into another callback that removes the file.

The net effect is to just download to downloads but the intermediate step is sending it to a folder first.

import dash
import plotly.graph_objects as go
from dash import Dash, dcc, html, Input, Output

app = Dash(__name__)
app.layout = html.Div([
    html.Button("Submit", id="submit-val"),
    html.Button("Download Plots", id="btn-plt-dwnl"),
    dcc.Download(id="btn-plt-dwnl")
])

@app.callback(
    [Output("fig-list", "value")],
    [Input("submit-val", "n_clicks")]
)
def graphing(n_clicks):
    if n_clicks != None:
       ...function to create plots
       fig_list = [fig1,fig2,fig3]
    return fig_list
#callback to remove the plot
@app.callback(
    Output("rmv-plt", "value"),
    Input("filepath-rmv", "value"),
    prevent_initial_call=True,
)
def rmv_plt(filepath):
    os.remove(rf"{filepath}")
    return dash.no_update


# Callback to download plots
@app.callback(
    [
        Output("btn-plt-dwnl", "data"),
        Output("filepath-rmv", "value"),
    ],
    Input("btn-downl-plts-html", "n_clicks"),
    State("fig-list", "value"),
    prevent_initial_call=True,
)
def dwnld_btn(n_clicks, fig_list):
    if n_clicks != None:
        print(len(fig_list))
        fig = go.Figure(fig_list[0])
        s = str(fig.full_figure_for_development)
        start = s.find("<b>") + len("<b>")
        end = s.find("</b>")
        substring = s[start:end]
        print(substring)
        # use the title of the graph as the figure download path
        filename = f"{substring}.html"
        filepath = rf"./charts/{filename}"
        # write the file to the charts folder but then send the filepath to the other callback to delete it
        with open(filepath, "a") as f:
            for fig in fig_list:
                # figs are dicts for some reason so convert back to figure
                fig = go.Figure(fig)
                f.write(fig.to_html(full_html=False, include_plotlyjs="cdn"))
        print(filename)
    return dcc.send_file(filepath), filepath

There is probably a better solution using bytes.io but I haven't tried that yet. If anyone has something better please let me know

Solution 2:[2]

I think this might be a better approach. Note that the error you were getting was caused by the brackets around the Output (If you leave it with the brackets, dash will expect to return an iterable from the function. That's why the code in your answer works)

@app.callback(
    Output("btn-dwnld", "data"),  # CARE THIS LINE
    [Input("btn-downl-plts-html", "n_clicks")],
    [State("fig-list", "value")],
)
def dwnld_btn(n_clicks, fig_list):
    if n_clicks != None:
        for fig in fig_list:
            # figs pull in as dicts for some reason so turn back to figure
            fig = go.Figure(fig)
            # use the title of the graph as the figure download path
            s = str(fig.full_figure_for_development)
            #the plot titles are nestled in bold tags
            start = s.find("<b>") + len("<b>")
            end = s.find("</b>")
            substring = s[start:end]
            filename = f"{substring}.html"
            fig.write_html(filename)
            print(filename)
            return dict(content=fig.to_html(), filename=filename)  # THIS ONE AS WELL

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 shawn
Solution 2 Manuel Montoya