Search code examples
pythonplotlygoogle-colaboratoryinteractive

How to update new line colours in Plotly from a button click and access results?


I want to plot an image, draw freehand over the image, then be able to press a custom button so that freehand drawing is now in a different colour. I cannot figure out how to make the button press change the line colour though.

The code I have tried is here below. I've tried using all four button methods described in the documentation, but none of them have any effect when pressed.

Furthermore, I can't find anywhere in the documentation how to access the lines that have been drawn over the image once finished (other than having to save the image manually using the GUI which I want to avoid).

# Imports
import plotly
import plotly.graph_objects as go
import plotly.express as px

# Show image
img = cv2.imread(fpath)
fig = px.imshow(img)

# Enable freehand drawing on mouse drag
fig.update_layout(overwrite=True,
    dragmode='drawopenpath',
    newshape_line_color='cyan',
    modebar_add=['drawopenpath',"eraseshape"])

# Add two buttons, 'r' and 'b' which attempt to update newshape_line_color...
fig.update_layout(
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            active=0,
            showactive=True,
            x=0.57,
            y=1.2,
            buttons=list([
                {
                     'label':"r",
                     'method':"relayout",
                     'args':[{'newshape_line_color':'red'}],
                },
                dict(label="b",
                     method="restyle",
                     args=[{"newshape_line_color": 'blue'}]),
            ]),
        )
    ])

# Show figure
config = dict({'scrollZoom': True})
fig.show(config = config)

Any help would be greatly appreciated!


Solution

  • If I understand correctly, you want all of the drawn lines to change color when the button is selected.

    I've got two solutions for you.

    The first doesn't do exactly what you're asking for, but it's entirely in Python. Instead of changing the last line drawn, all subsequent lines are drawn with the selected color.

    The second does do what you're looking for but requires JS.

    Pythonic...but not quite what you're looking for

    Here's the updated updatemenus. You were pretty close, actually. I've created two color buttons: red and green.

    fig.update_layout(
        updatemenus = list([
            dict(type = "buttons",
                 direction = "right",
                 active = 0,
                 showactive = True,
                 x = 0.57,
                 y = 1.2,
                 buttons = list([   # change future colors
                    dict(label = "Make Me Red", 
                         method = "relayout", 
                         args = [{'newshape.line.color': 'red'}]
                         ),
                    dict(label = "Make Me Green", 
                         method = "relayout", 
                         args = [{'newshape.line.color': 'green'}]
                         )
                 ])  
            )
        ])
    )
    

    When you select the color button, all lines drawn after will be in the color selected.

    enter image description here

    Here's the entire chunk of code used to make this:

    from skimage import io
    import plotly.graph_objects as go
    import plotly.express as px
    
    # Show image
    img = io.imread('https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Crab_Nebula.jpg/240px-Crab_Nebula.jpg')
    fig = px.imshow(img)
    
    # Enable freehand drawing on mouse drag
    fig.update_layout(overwrite=True,
        dragmode='drawopenpath',
        newshape_line_color='cyan',
        modebar_add=['drawopenpath',"eraseshape"])
    
    fig.add_shape(dict(editable = True, type = "line", 
                       line = dict(color = "white"), 
                       layer = 'above',
                       x0 = 0, x1 = 200.0000001, 
                       y0 = 0, y1 = 200.0000001))
    
    # Add two buttons, 'r' and 'b' which attempt to update newshape_line_color...
    fig.update_layout(
        updatemenus = list([
            dict(type = "buttons",
                 direction = "right",
                 active = 0,
                 showactive = True,
                 x = 0.57,
                 y = 1.2,
                 buttons = list([   # change future colors
                    dict(label = "Make Me Red", 
                         method = "relayout", 
                         args = [{'newshape.line.color': 'red'}]
                         ),
                    dict(label = "Make Me Green", 
                         method = "relayout", 
                         args = [{'newshape.line.color': 'green'}]
                         )
                 ])  
            )
        ])
    )
    
    # Show figure
    config = dict({'scrollZoom': True})
    fig.show(config = config)
    

    Embedded JS Changing the Drawn Line Color

    You'll use everything up to fig.show() then you'll use the following. Yes, it creates an external file, but it will immediately open in your browser, as well.

    This piggybacks off of your buttons. When green is clicked now, it will change all of the lines, not just what's drawn next. There are two events here, one for each color.

    In the JS, you'll notice two for loops in each event. These serve very different purposes. Because there doesn't seem to be a built-in event to do this for me, the first loop changes the actual attributes of the plot. However, that won't be visible immediately. So the second loop changes what you actually see at that moment.

    This requires the Plotly io package. You had called import plotly, so you could just change pio to plotly.io instead of calling it though.

    import plotly.io as pio
    
    pio.write_html(fig, file = 'index2.html', auto_open = True, 
                config = config, include_plotlyjs = 'cdn', include_mathjax = 'cdn',
                post_script = "setTimeout(function() {" +
                "btns = document.querySelectorAll('g.updatemenu-button');" +
                "btns[0].addEventListener('click', function() {" + 
                    "ch = document.getElementById('thisCh');" +
                    "shapes = ch.layout.shapes;     /* update the plot attributes */" +
                    "for(i = 0; i < shapes.length; i++) {" +
                        "shapes[i].line.color = 'red';" +
                    "}                 /* update the current appearance immediately */" +
                    "chart = document.querySelectorAll('g.shapelayer')[2];" +
                        "for(i = 0; i < chart.children.length; i++) {" +
                            "chart.children[i].style.stroke = 'red';" +
                        "}" +
                "});" +
                "btns[1].addEventListener('click', function() {" +
                    "ch = document.getElementById('thisCh');" +
                    "shapes = ch.layout.shapes;     /* update the plot attributes */" +
                    "for(i = 0; i < shapes.length; i++) {" +
                        "shapes[i].line.color = 'green';" +
                    "}               /* update the current appearance immediately */" + 
                    "chart = document.querySelectorAll('g.shapelayer')[2];" +
                    "for(i = 0; i < chart.children.length; i++) {" +
                        "chart.children[i].style.stroke = 'green';" +
                    "}" +
                "});" +
                "}, 200)", full_html = True, div_id = "thisCh")
    

    enter image description here

    enter image description here

    If I've misunderstood what you're looking for or if you have any questions, let me know. Oh, and if you go with the second solution but wanted many colors, I can make it color dynamic, I didn't do that with only two colors that I used for this demonstration.