Adding an SVG image (and listen to events on the image)

Summary

Add into streamlit an svg image that can provide interactivity.

More specifically, I am looking to add an svg image generated by Graphviz into my streamlit project.

Furthermore, be able to react to clicks on such an image.

Find details in this github: GitHub - susuhahnml/streamlit-issue: Describing the streamlit feature-issue

I tried something as suggested in Display SVG - #4 by jeremie

Steps to reproduce

Streamlit app

import streamlit as st
import base64

def render_svg(svg):
    """Renders the given svg string."""
    b64 = base64.b64encode(svg.encode('utf-8')).decode("utf-8")
    html = r'<img src="data:image/svg+xml;base64,%s"/>' % b64
    c = st.container()
    c.write(html, unsafe_allow_html=True)

render_svg(open("issue/default.svg").read())

I can not listen to the clicks on the svg image

SVG to include

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 8.1.0 (20230707.0739)
 -->
<!-- Title: default Pages: 1 -->
<svg width="62pt" height="116pt"
 viewBox="0.00 0.00 62.00 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">
<title>default</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-112 58,-112 58,4 -4,4"/>
<!-- b -->
<g id="node1" class="node">
<title>b</title>
<ellipse fill="none" stroke="black" cx="27" cy="-18" rx="27" ry="18"/>
<text text-anchor="middle" x="27" y="-12.95" font-family="Times,serif" font-size="14.00">b</text>
</g>
<!-- a -->
<g id="node2" class="node">
<title>a</title>
<polygon fill="none" stroke="red" points="54,-108 0,-108 0,-72 54,-72 54,-108"/>
<text text-anchor="middle" x="27" y="-84.95" font-family="Times,serif" font-size="14.00">a</text>
</g>
<!-- a&#45;&#45;b -->
<g id="edge1" class="edge">
<title>a&#45;&#45;b</title>
<path fill="none" stroke="black" d="M27,-71.7C27,-60.85 27,-46.92 27,-36.1"/>
</g>
</g>

<script>
      <script type="text/javascript">

        var nodes = Object.values(document.getElementsByClassName('node'));
        window.onload=function(){
            nodes.forEach(node=>{
                node.addEventListener("click", function() {
                    console.log("On click")
                    console.log(node)
                    node.style['opacity']='0.2'
                })

            })

        }
      </script>

</script>
</svg>

Debug info

  • Streamlit version: 1.26
  • Python version: 3.10.12
  • Using Conda
  • OS version: MAC OS
  • Browser version: Same for different edditions

Other

I also tried adding the svg directly like described in the github but the svg componets are reordered

1 Like

Hello

Fyi, st.write and st.markdown do not execute JavaScript. To execute JavaScript you can use the streamlit.components.v1.html function or a third party custom component like streamlit-javascript. Note that both of these work using iframes meaning that the code is executed inside an iframe and thus doesnt really have much access to whats outside of the iframe like all the other components in your Streamlit app.

Try this instead:

import streamlit as st
import base64
from streamlit.components.v1 import html

def render_svg(svg_string):
    """Renders the given svg string."""
    c = st.container()
    with c:
        html(svg_string)

render_svg(open("issue/default.svg").read())

Also, is there a reason your script is nested inside another script in your SVG code?

1 Like

Thank you @bouzidanas for the answer! This is already a big step in the direction I need. I see that now I can access the elements inside the iframe from an outside script, as long as I can get the corresponding iframe. I was playing around a plain html, and I can get the iframe if I add an id to it. By doing so I would need to add a script like this to my whole streamlit application.

   <script type="text/javascript">

    var iframe = document.getElementById("yourIframe");
    console.log(iframe)
    var nodes = Object.values(iframe.contentWindow.document.body.getElementsByClassName('node'))
    console.log(nodes)
    window.onload=function(){
        nodes.forEach(node=>{
            node.addEventListener("click", function() {
                console.log("On click")
                console.log(node)
                node.style['opacity']='0.2'
            })

        })

    }
    </script>

Then my questions would be the following:

  • Is there a way to access get the iframe created by html(svg_string), either via an id or something else?
  • How can I add the script above the the streamlit application, do I need to do it again with the html?

Thank you in advance! Full disclosure I am a backend developer so I might be asking some trivial frontend questions. :slight_smile:

Sorry, there might be a little confusion here but I am not sure. Your best bet is to basically put your svg and javascript inside an iframe and have the JavaScript executed therein. The html function does that all for you. If you inspect the streamlit app in your browser, you can see that the html function puts your svg (and javascript) inside an iframe.

The Streamlit app still has the ability to change/control the contents of the iframe in the normal way which is to change it on rerun (remember, the python script is the backend) but the difficult part is to have the svg affect the rest of the app on the frontend. For this to be achieved, the iframe or component containing the iframe must communicate back to the backend script (Streamlit script) which then makes appropriate changes to the app frontend. So the iframe/component must send info back to the Streamlit script (like say when an svg element is clicked) so that the script can then change the app accordingly.

As far as I know, the html function that I recommended does NOT have bidirectional functionality (it doesnt return anything for you to respond to ie it doesnt talk back to the script; it just adds the content you give it wrapped in an iframe). However, the 3rd party component that I referenced (streamlit-javascript) apparently does have this ability. Which means you can send back “results” which could be the name of the element you clicked in your case for example which the script can then respond to. The problem with this 3rd party component is that it ONLY executes javascript so it probably isnt what you are looking for.

I have some good news and bad news. The good news is that if you understand how components communicate back to the backend, then you can do so with not too much additional javascript and python code. The bad news is that its not super intuitive and I dont remember the details myself. Just that its done using window.parent.postMessage.

You might find more info/clues in posts like this and this and this

Hopefully all this makes a little more sense.

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