Download graphs to temporary directory - streamlit elements

I’ve made a dashboard with streamlit elements and use this js code in the callback on buttons to download:

js_code = f"""
                function saveGraphAsPNG() {{
                    var box = document.getElementById('{unique_id}');
                    if (!box) {{
                        alert('No box found to export');
                        return;
                    }}
                    var svg = box.querySelector('svg'); // Select the SVG within the specific box
                    if (!svg) {{
                        alert('No SVG found within the box');
                        return;
                    }}
                    
                    // Get the SVG's bounding box dimensions
                    var bbox = svg.getBBox();
                    var svgWidth = bbox.width;
                    var svgHeight = bbox.height;
                    
                    // Define the DPI scaling factor (e.g., 2 for 2x resolution)
                    var dpiScalingFactor = {scaling_factor}; // Adjust as needed
                    
                    // Create a canvas with the dimensions of the SVG
                    var canvas = document.createElement('canvas');
                    canvas.width = svgWidth * dpiScalingFactor;
                    canvas.height = svgHeight * dpiScalingFactor;
                    var ctx = canvas.getContext('2d');
                    
                    // Set canvas scale to match the DPI scaling factor
                    ctx.scale(dpiScalingFactor, dpiScalingFactor);
                    
                    // Convert the SVG to a PNG image and draw it on the canvas
                    var svgData = new XMLSerializer().serializeToString(svg);
                    var img = new Image();
                    img.onload = function() {{
                        ctx.drawImage(img, 0, 0);
                        var png = canvas.toDataURL('image/png');
                        var a = document.createElement('a');
                        a.href = png;
                        a.download = '{filename}.png';
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                    }};
                    img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
                }}

                """

where unique_id is the id of the box container. This works perfectly.

However, I’m trying to adapt a callback so that all the svgs on the page created by elements is downloaded into a temporary directory which I can later use to create an automated report.

I have looked and everywhere is saying you need to send the data from the frontend to the backend using flask, but I’m aware that flask doesnt work with streamlit very well.

Another proposal was to use session state with this code:

f"""
                function captureNivoChartInBox() {{
                    var box = document.getElementById('{box_id}');
                    if (!box) {{
                        console.error(`Box with ID not found.`);
                        return;
                    }}
                    var svg = box.querySelector('svg'); // Select the SVG within the specific box
                    if (!svg) {{
                        console.error(`No SVG found within box with ID.`);
                        return;
                    }}

                    // Capture the SVG using html2canvas or another method
                    html2canvas(box).then(canvas => {{
                        var imgData = canvas.toDataURL('image/png');
                        
                        // Send the Base64 image data back to Python
                        const imageId = boxId + '_image';
                        var inputElement = document.createElement('input');
                        inputElement.setAttribute('type', 'hidden');
                        inputElement.setAttribute('name', imageId);
                        inputElement.setAttribute('value', imgData);
                        
                        var form = document.createElement('form');
                        form.appendChild(inputElement);
                        document.body.appendChild(form);

                        // Use Streamlit to handle the data
                        form.submit();
                    }});
                }}
            """

However, I can’t seem to find the data in the session state.

Has anyone got any ideas?

Could you share what you’ve tried as a complete reproducible script? I see the JS code you’re using, but I don’t know how exactly it fits in a streamlit app.

Certainly!

These are the constants I’ll be using for the demo.

data = [
  {
    "country": "AD",
    "hot dog": 126,
    "hot dogColor": "hsl(109, 70%, 50%)",
    "burger": 65,
    "burgerColor": "hsl(281, 70%, 50%)",
    "sandwich": 12,
    "sandwichColor": "hsl(163, 70%, 50%)",
    "kebab": 129,
    "kebabColor": "hsl(271, 70%, 50%)",
    "fries": 50,
    "friesColor": "hsl(240, 70%, 50%)",
    "donut": 30,
    "donutColor": "hsl(124, 70%, 50%)"
  },
  {
    "country": "AE",
    "hot dog": 158,
    "hot dogColor": "hsl(350, 70%, 50%)",
    "burger": 72,
    "burgerColor": "hsl(172, 70%, 50%)",
    "sandwich": 21,
    "sandwichColor": "hsl(68, 70%, 50%)",
    "kebab": 123,
    "kebabColor": "hsl(322, 70%, 50%)",
    "fries": 24,
    "friesColor": "hsl(186, 70%, 50%)",
    "donut": 170,
    "donutColor": "hsl(183, 70%, 50%)"
  }
]

unique_id = "Bar"

What I have that works with a download button:

import streamlit as st

from streamlit_elements_fluence import (
    mui,
    elements,
    JSCallback,
    nivo
)


download_chart_js = f"""
    function saveGraphAsPNG() {{
        var box = document.getElementById('{unique_id}');
        if (!box) {{
            alert('No box found to export');
            return;
        }}
        var svg = box.querySelector('svg'); // Select the SVG within the specific box
        if (!svg) {{
            alert('No SVG found within the box');
            return;
        }}
        
        // Get the SVG's bounding box dimensions
        var bbox = svg.getBBox();
        var svgWidth = bbox.width;
        var svgHeight = bbox.height;
        
        // Define the DPI scaling factor (e.g., 2 for 2x resolution)
        var dpiScalingFactor = 2; // Adjust as needed
        
        // Create a canvas with the dimensions of the SVG
        var canvas = document.createElement('canvas');
        canvas.width = svgWidth * dpiScalingFactor;
        canvas.height = svgHeight * dpiScalingFactor;
        var ctx = canvas.getContext('2d');
        
        // Set canvas scale to match the DPI scaling factor
        ctx.scale(dpiScalingFactor, dpiScalingFactor);
        
        // Convert the SVG to a PNG image and draw it on the canvas
        var svgData = new XMLSerializer().serializeToString(svg);
        var img = new Image();
        img.onload = function() {{
            ctx.drawImage(img, 0, 0);
            var png = canvas.toDataURL('image/png');
            var a = document.createElement('a');
            a.href = png;
            a.download = 'graph.png';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }};
        img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
    }}

    """
    
    
with elements("example_dash"):
    
    with mui.Stack(
                className="Draggable",
                alignItems="center",
                direction="row",
                spacing=1,
                sx={
                    "padding": "5px 15px 5px 15px",
                    "borderBottom": 1,
                    "borderColor": "divider",
                },
            ):
                
            mui.Typography("Some graph", sx={"flex": 1})

            # download chart button
            mui.Button(
                "",
                startIcon=mui.icon.Download(),
                size="small",
                variant="text",
                color="info",
                style={"color" : "#000000", "background":"#FFFFFF"},
                onClick=JSCallback(
                    download_chart_js
                ),
            )
            
    with mui.Box(id=unique_id, sx={
                                "flex": 1,
                                "minHeight": 50,
                                "height" : 200
                                }):
            nivo.Bar(
                data=data,
                keys=[
                    'hot dog',
                    'burger',
                    'sandwich',
                    'kebab',
                    'fries',
                    'donut'
                ],
                indexBy='country'
            )

What I want to have to save a png to a temporary folder without clicking a button (if I had something like this I’d loop through a list of unique_ids to download multiple charts of interest):

import base64
from io import BytesIO
from PIL import Image
import time

import streamlit as st
import streamlit.components.v1 as components

from streamlit_elements_fluence import (
    mui,
    elements,
    JSCallback,
    nivo
)

from streamlit_javascript import st_javascript




with elements("example_dash"):
    
    with mui.Stack(
                className="Draggable",
                alignItems="center",
                direction="row",
                spacing=1,
                sx={
                    "padding": "5px 15px 5px 15px",
                    "borderBottom": 1,
                    "borderColor": "divider",
                },
            ):
                
            mui.Typography("Some graph", sx={"flex": 1})

            # download chart button
            mui.Button(
                "",
                startIcon=mui.icon.Download(),
                size="small",
                variant="text",
                color="info",
                style={"color" : "#000000", "background":"#FFFFFF"},
                # No callback!
            )
            
    with mui.Box(id=unique_id, sx={
                                "flex": 1,
                                "minHeight": 50,
                                "height" : 200
                                }):
            nivo.Bar(
                data=data,
                keys=[
                    'hot dog',
                    'burger',
                    'sandwich',
                    'kebab',
                    'fries',
                    'donut'
                ],
                indexBy='country'
            )
            
    box_id = unique_id  # the same ID as the box where the chart is

    # Some js to save the bytes data in the page
    setting_chart_to_page_variable_js = f"""
    <script>
        var box = document.getElementById({box_id});
        alert('Yes');
        if (box) {{
            var svg = box.querySelector('svg'); // Select the SVG within the specific box
            if (svg) {{
                html2canvas(box).then(canvas => {{
                    var imgData = canvas.toDataURL('image/png');
                    // Return the Base64 image data to be called later
                    window.myImgData = imgData;
                }}).catch(error => {{
                    console.error('Error capturing the chart:', error);
                    window.myImgData = 0;
                }});
            }} else {{
                console.error('No SVG found within box with ID {box_id}.');
                window.myImgData = 0;
            }}
        }} else {{
            console.error('Box with ID {box_id} not found.');
            window.myImgData = 0;
        }}
    </script>
    """
    
    components.html(setting_chart_to_page_variable_js)








# Attempt to retrieve the JavaScript myImgData's into python

unique_while_key = 0
while True:
    unique_while_key += 1
    image_data = st_javascript('parent.window.myImgData', key=unique_while_key)
    
    if image_data !=0 :
        break
    
    # Wait 1 second before trying again
    time.sleep(1)
    
    
# Convert the base64 image data and save
if image_data:

    # Decode the Base64 image data
    img_data = base64.b64decode(image_data.split(",")[1])
    img = Image.open(BytesIO(img_data))
    
    # Save the image as a PNG file
    img_path = f"{box_id}.png"
    img.save(img_path)
    
    st.success(f"Image saved as {img_path}")
    st.image(img, caption="Captured Chart")
else:
    st.warning("No image data returned from JavaScript.")

bump

Hello, I don’t use JS, only python, but could’t use the session_state to store them ? (it’s what i’m doing with the dashboard, it’s working fine)

How would you save a chart to the session state?

st.session_state.chart = nivo.Bar… ?

Please show me the code you have done

self.figures_data.append({"selected_figure_name": selected_figure_name, "figure": figure})

To load the figure in cache

figure = st.session_state[figure_key]
self.figure_display(figure, col)
    def figure_display(self, figure, col):
        """Display the figure, if it's a matplotlib, or graphviz or a dataframe"""

        # Try to display a Matplotlib figure
        try:
            if hasattr(figure, 'figure'):
                col.pyplot(figure.figure)
                return
        except Exception as e1:
            # Continue to the next type of figure
            error_message = f"Matplotlib display error: {e1}"

        # Try to display a Pandas DataFrame
        try:
            if isinstance(figure, pd.DataFrame):
                col.dataframe(figure)
                return
        except Exception as e3:
            # Continue to the final error handling
            error_message = f"Pandas DataFrame display error: {e3}"

        # If none of the above worked, display an error message
        col.write(f"Error displaying figure or chart: {error_message}")

It’s a bit specific to my code, but i’m sure you can adapt it to another librairy

No unfortunately, since I’m using a python to react interface. When I call my bar method it converts it to javascript and sends it to react. So I cannot get the react chart into a python object.

You can update with a png in the session_state :

        uploaded_file = YOURPNGFILE

        # Check if the image is already stored in session_state, otherwise read and store it
        if 'uploaded_image' not in st.session_state:
            try:
                # Open the image file in binary mode and store the content in session_state
                with open(uploaded_file, 'rb') as file:
                    st.session_state.uploaded_image = file.read()
            except FileNotFoundError:
                st.error("Image file not found!")

        # Display the image if it is successfully stored in session_state
        if st.session_state.get('uploaded_image') is not None:
            st.image(st.session_state.uploaded_image, caption="Loaded Image", use_column_width=True)
        else:
            st.write("No image available.")

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.