Create a file in memory + start downloading upon clicking a button

I encountered the following situation:
My app is performing some calculations and displaying multiple plots at the same time. There are a couple of input widgets in the sidebar, which influence how the plots look like. So every time one of those input widgets is changed, the plots get rerendered.

Now, I would like to offer a button to download all of the plots currently shown as a zip file. Currently I implemented this using st.download_button and passing the zipfile data as data parameter (the zip file is created in memory using BytesIO).
It is working, however the problem is, that everytime an input widget changes and the plots change, the zipfile will be recreated which slows down the user experience - even though most of the time the user will not actually download the file.

I am rather looking for a solution to not only start the download by clicking a button, but also triggering the creation of the zip file only upon clicking the button.
How would you go about implementing this? I guess you’d need some kind of javascript?

Dummy code:

def figs2zip(figs) -> bytes:
    # create zip file containing all figs in memory using io.BytesIO()
    # return zip file buffer
    return zip_buf

# some input widgets
# create multiple plots using matplotlib and display them
for fig in figs:

# => this is slow, because figs2zip() will run on EVERY script rerun and
# always create a new zip file in memory
st.download_button(label='Download plots', data = figs2zip(fig), filename='',  mime='application/zip')

I would much rather want a behavior like this:

if st.button('Download plots'):
    zip_buf = figs2zip(fig)
    trigger_download(zip_buf)    # <--- How do you implement this? 🤨

I.e, the zip file gets only created when actually clicking the button, rather than on every script run. At the same time, after creating the file in memory the download dialogue should pop-up just like with st.download_button.

1 Like