Download Plotly figures as PDF with download_button

Hi!

I’m trying to figure out the [best] way to download my plotly figures saved in session state with the download button.

I do a similar thing with saved data frames for which I have the following code:

@st.cache
def save_df(df: pandas.DataFrame):
    return df.to_csv(sep='\t', index=False).encode('utf-8')

# some app code here

if not st.session_state.processed_data and not st.session_state.final_feature_table:
    st.warning('Process data!')

else:
    files = {
        "Feature dataframe": st.session_state.final_feature_table[0],
        "Processed dataframe": st.session_state.processed_data[0]
    }

    available_files = [key for (key, value) in files.items()]
    file_name = st.text_input('', value='your_filename')
    selected_file = st.selectbox('Select a file for download:', available_files, index=0)
    file = save_df(files[selected_file])

    st.download_button(
        label="Download table",
        data=file,
        file_name=f'{file_name}.tab',
        mime='text/csv'
    )

This works like a charm.

Now, I generate figures using a form, resetting the state after each re-submission. I check and it works outside of the form using st.plotly_chart.

The problem occurs when I try to save a static figure to a PDF file using the following code:

@st.cache
def save_figure(figure: go.Figure, file):
    return figure.write_image(file, format='pdf')


# this is outside of the form

    if not st.session_state.figures:
        st.warning('Generate figures!')
    else:
        st.markdown('## Download Figures')
        available_figures = [key for (key, value) in st.session_state.figures.items()]
        file_name = st.text_input('', value='your_filename')
        selected_figure = st.selectbox('Select a figure for download:', available_figures, index=0)
        figure_object = st.session_state.figures[selected_figure]  # I can see this figure with 
        save_figure = save_figure(figure_object, file_name)
        btn = st.download_button(
            label="Download image",
            data=save_figure,
            file_name=f"{file_name}.pdf",
            mime="figure/pdf")

I get the this error:

RuntimeError: Invalid binary data format: <class 'NoneType'>

I’m guessing it’s something trivial that I’m missing but I just cannot figure out what it is!

Thanks,
Simon

Hi @simonb :wave:

The docs for figure.write_image states that the function returns None. To get this working, let’s save the generated pdf of the figure in an in-memory buffer using io.BytesIO(), and download the buffer contents with st.download_button. Note: the write_image function also requires the installation of the kaleido package.

Here’s a working reproducible example:

Solution

import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import io

# Load the data
@st.experimental_memo
def load_data():
    return pd.DataFrame(
        {
            "Fruit": ["Apples", "Oranges", "Bananas", "Apples", "Oranges", "Bananas"],
            "Contestant": ["Alex", "Alex", "Alex", "Jordan", "Jordan", "Jordan"],
            "Number Eaten": [2, 1, 3, 1, 3, 2],
        }
    )

# Create and cache a Plotly figure
@st.experimental_memo
def create_figure(df):
    fig = go.Figure()
    for contestant, group in df.groupby("Contestant"):
        fig.add_trace(
            go.Bar(
                x=group["Fruit"],
                y=group["Number Eaten"],
                name=contestant,
                hovertemplate="Contestant=%s<br>Fruit=%%{x}<br>Number Eaten=%%{y}<extra></extra>"
                % contestant,
            )
        )
    fig.update_layout(legend_title_text="Contestant")
    fig.update_xaxes(title_text="Fruit")
    fig.update_yaxes(title_text="Number Eaten")
    return fig

df = load_data()
fig = create_figure(df)

# Create an in-memory buffer
buffer = io.BytesIO()

# Save the figure as a pdf to the buffer
fig.write_image(file=buffer, format="pdf")

# Download the pdf from the buffer
st.download_button(
    label="Download PDF",
    data=buffer,
    file_name="figure.pdf",
    mime="application/pdf",
)

st.plotly_chart(fig)

Output

plotly-download-pdf

Happy Streamlit-ing! :balloon:
Snehan

2 Likes

Thank you @snehankekre, app’s working beautifully now! :vulcan_salute:t2:

Simon

1 Like

Hi @snehankekre
Thanks for this post. I was wondering how I can accomplish, using the same technique you described, to export multiple plots to pdf.

I have a function that returns a plot, but because I am grouping by users, it renders different contour plots for each user. I would like to save all these multiple plots generated by contour_plot(df) function in a single pdf report: See the code below:

def contour_plot(df):  
    fig_name = 0
    list_ = []
    groups = df.groupby('User')
    # loop over each group
    for name, group in groups:
        x = np.array(group['col1'])
        y = np.array(group['col2'])
        z = np.array(group['col3'])
        xi = np.linspace(x.min(), x.max(), 100)
        yi = np.linspace(y.min(), y.max(), 100)
        grid_x, grid_y = np.meshgrid(xi,yi)
        Z = griddata((x,y), z, (grid_x, grid_y), method='linear')   
        fig = go.Figure(go.Contour(x=xi, y=yi, z=Z, colorscale='Jet')
        cached_dict = {fig_name: fig}        
        st.plotly_chart(cached_dict[fig_name],
                        use_container_width=True,
                        theme="streamlit")        
        fig_name += 1
        list_.append(cached_dict)
    return list_

Now, if I write the code as follows, it will save only the first plot to pdf, but probably I need to write a loop to be able to write all the plots to pdf:

buffer = io.BytesIO()
fig_plot = contour_plot(df=df)
fig_plot[0][0].write_image(file=buffer, format="pdf")

I tried this, but it didn’t work:

for i in range(len(fig_plot)):
    fig_plot[i][i].write_image(file=buffer, format="pdf")

Appreciate any feedback.

Hello @serdar_bay, I am facing exactly this issue in my application.

Can I ask you if you encounter a solution to save mutliple images ?