Summary
My app is showing multiple plots at once. The plots can change depending on the user input through widgets on the sidebar. Every change to those widgets leads to a new script run from top to bottom in which the plots get rendered again.
Problem:
I want to include a download button on the top of the page providing a zip file of those plots. Now, since the script is re-run every time the user input changes, the zipfile will be created over and over again. Depending on the number of plots, this can be expensive.
How can I trigger the generation AND download of the zip file only upon clicking a button?
Code
MWE:
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
# create zip file from list of figs
def figs2zip(figs: list[plt.figure]) -> bytes:
"""THIS WILL BE RUN ON EVERY SCRIPT RUN"""
import io
import zipfile
zip_buf = io.BytesIO()
with zipfile.ZipFile(file=zip_buf, mode='w', compression=zipfile.ZIP_DEFLATED) as z:
for i,fig in enumerate(figs):
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight')
filename = f'{i}.png'
z.writestr(zinfo_or_arcname=filename, data=buf.getvalue() )
buf.close()
buf = zip_buf.getvalue()
zip_buf.close()
return buf
# some plots
x = np.linspace(0, np.pi*2, 50)
fig1, ax1 = plt.subplots(1,1)
ax1.plot(x, np.sin(x))
fig2, ax2 = plt.subplots(1,1)
ax2.plot(x, np.cos(x))
# show plots on page
st.markdown('# Just some plots')
placeholder = st.empty()
st.markdown('### Sin')
st.pyplot(fig1)
st.markdown('### Cos')
st.pyplot(fig2)
# show download button on top
with placeholder:
st.download_button(label='Download plots', data=figs2zip([fig1, fig2]), file_name='plots.zip')
The above code works, but will create a zip file every time the script is run again from top to bottom.
I want to only create the zip file upon clicking on a button and directly trigger its download. So I replaced the with placeholder:
part with some html/javascript by @snehankekre I found here Automatic Download / Select and Download File with Single Button Click - #4 by snehankekre .
# show download button on top
with placeholder:
def trigger_download(data, filename) -> str:
import base64
b64 = base64.b64encode(data).decode()
dl_link = f"""
<html>
<head>
<script src="http://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
$('<a href="data:application/octet-stream;base64,{b64}" download="{filename}">')[0].click()
</script>
</head>
</html>"""
return dl_link
def callback_button() -> None:
import streamlit.components.v1 as components
trigger = trigger_download(figs2zip([fig1, fig2]), "plots.zip")
components.html(html=trigger, height=0, width=0)
return
st.button('Download plots', on_click=callback_button)
While this technically works, it will lead to some ugly space added at the top after clicking the download button:
What is the proper way to solve this problem?