2

So I have this dash app where I want to display a png image based on the user's input. It works, but the problem is every time the user makes a selection the image is shown on top of the previous image. I want to somehow clear the previous image so it only shows the most recently selected image.

In app.layout I have:

app.layout = html.Div(children=[
    html.H4(children='Spider Plot'),
        dcc.Dropdown(id="select_group",
                 options=[
                     {"label": "Student", "value": 'Student'},
                     {"label": "Parent", "value": 'Parent'},
                     {"label": "Both", "value": 'Both'}],
                 multi=False,
                 value="Student",
                 style={'width': "40%"}
                 ),
    
    html.Div(id="spider_img", children=[]),
])

And for the callback I have:

@app.callback(
     Output(component_id='spider_img', component_property='children'),
    Input(component_id='select_group', component_property='value')
)

def update_graph(group):
    key = (2002, group)
    A = perm_to_series(Ds.loc[key,'D'],Ds.loc[key,'details_fixed_cont_x_minimize']['perm'],'Closest')
    B = perm_to_series(Ds.loc[key,'D'],Ds.loc[key,'details_fixed_cont_x_maximize']['perm'],'Farthest')
    pyrankability.plot.spider2(A,B,file='/tmp/spider3.png')
    return html_image(open('/tmp/spider3.png','rb').read())

I the function html_image is defined by me because apparently this is the way to insert static png images in dash.

def html_image(img_bytes):
    encoding = b64encode(img_bytes).decode()
    img_b64 = "data:image/png;base64," + encoding
    return html.Img(src=img_b64, style={'height': '30%', 'width': '30%'})

This does seem like a kind of hacky way to do things, and if there is a better way let me know, but this is the only thing that worked for me. So when looking for how to clear previous output I thought it would be simple, but I didn't really find much. There are posts that show how to clear a plot by clicking on it such as here but that's not what I want, I just want the previous image to be cleared so they don't overlap. How can I clear my component so it displays properly?

Edit: Here is the updated code using html.Img and component_property='src':

app.layout = html.Div(children=[
    html.H4(children='Spider Plot'),
        dcc.Dropdown(id="select_group",
                 options=[
                     {"label": "Student", "value": 'Student'},
                     {"label": "Parent", "value": 'Parent'},
                     {"label": "Both", "value": 'Both'}],
                 multi=False,
                 value="Student",
                 style={'width': "40%"}
                 ),
    
    html.Img(id="spider_img", style={'height': '30%', 'width': '30%'})
])

@app.callback(
    Output(component_id='spider_img', component_property='src'),
    Input(component_id='select_group', component_property='value')
)

def update_graph(group):
    key = (2002, group)
    A = perm_to_series(Ds.loc[key,'D'],Ds.loc[key,'details_fixed_cont_x_minimize']['perm'],'Closest')
    B = perm_to_series(Ds.loc[key,'D'],Ds.loc[key,'details_fixed_cont_x_maximize']['perm'],'Farthest')
    pyrankability.plot.spider2(A,B,file='/tmp/spider3.png')
    img = open('/tmp/spider3.png','rb').read()
    return "data:image/png;base64," + base64.b64encode(img).decode()
dumbitdownjr
  • 185
  • 3
  • 12
  • 1
    you could use `io.BytesIO` to save image in memory instead of disk - and later you can use it to read from memory instead of disk to create `base64` – furas Jun 14 '21 at 22:24

1 Answers1

2

To update existing image you should use html.Img(...) instead of html.Div(..., children=[]) in app.layout, and update component_property='src' instead of component_property='children'


Many tools can save image/file in file-like object created in memory with io.BytesIO()

Example for matplotlib

    # plot something
    plt.plot(...)

    # create file-like object in memory        
    buffer_img = io.BytesIO()

    # save in file-like object
    plt.savefig(buffer_img, format='png')

    # move to the beginning of buffer before reading (after writing)
    buffer_img.seek(0)
    
    # read from file-like object
    img_bytes = buffer_img.read()

    # create base64
    img_encoded = "data:image/png;base64," + base64.b64encode(img_bytes).decode()

Minimal working code

import dash
import dash_core_components as dcc
import dash_html_components as html
import base64
import io
import matplotlib.pyplot as plt

app = dash.Dash()

app.layout = html.Div(children=[
    html.H4(children='Spider Plot'),
        dcc.Dropdown(id="select_group",
                 options=[
                     {"label": "Student", "value": 'Student'},
                     {"label": "Parent", "value": 'Parent'},
                     {"label": "Both", "value": 'Both'}],
                 multi=False,
                 value="Student",
                 style={'width': "40%"}
                 ),
    html.Img(id="spider_img", style={'height': '30%', 'width': '30%'}),
])

@app.callback(
     dash.dependencies.Output(component_id='spider_img', component_property='src'),
     dash.dependencies.Input(component_id='select_group', component_property='value')
)
def update_graph(group):
    # plot 
    plt.clf()
    plt.text(5, 5, group, size=20)
    plt.xlim(0, 15)
    plt.ylim(0, 10)

    # create file-like object in memory        
    buffer_img = io.BytesIO()

    # save in file-like object
    plt.savefig(buffer_img, format='png')

    # move to the beginning of buffer before reading (after writing)
    buffer_img.seek(0)
    
    # read from file-like object
    img_bytes = buffer_img.read()

    # create base64
    img_encoded = "data:image/png;base64," + base64.b64encode(img_bytes).decode()

    return img_encoded

if __name__ == '__main__':
    app.run_server(debug=False)
furas
  • 134,197
  • 12
  • 106
  • 148
  • Great answer, however unfortunately the way the library I'm working with works is that I need to write the png to file for it to work. The weird thing is I tried changing things to `html.Img()` and `component_property=src`, but it still displays the images on top of eachother. Should those two changes have fixed that? – dumbitdownjr Jun 15 '21 at 01:23
  • `.Output(component_id='spider_img', component_property='src')` should replace image (`src`) in `html.Img(id="spider_img")` - if you returns string `base64` instead of another `Img()`. If you still have images on top of eachother then maybe you have other code which still adds it. OR you have other element with `id="spider_img"` and it puts it in wrong place. – furas Jun 15 '21 at 01:33
  • I couldn't find library `pyrankability` on internet to see its code without downloading - maybe it would need some changes in source code to work with `io.BytesIO` – furas Jun 15 '21 at 01:35
  • That's what I thought too, but it just layers the images on top of eachother. I added my updated code at the bottom of the post (which for some reason gives me an error when i take the encoding/decoding out of a function but that's not the main issue). And I only have the one element with that id. And yes, pyrankability is a private library for now and it's based on some legacy code. I might look into changing that later, but for now I just want to fix this overlaying images issue :) – dumbitdownjr Jun 15 '21 at 01:58
  • updated code seems OK - the only idea that browser uses some cache and load old page from cache. – furas Jun 15 '21 at 02:28
  • I'm not sure either, bu thanks a lot for the help! I'll see if I can figure it out. – dumbitdownjr Jun 15 '21 at 02:32
  • I think the issue is that the image itself is being appended too essentially, so whenever the image is updated it writes on top of the old one to file. I tried deleting the file in between updates if it exists, but that didn't fix it which is strange... – dumbitdownjr Jun 15 '21 at 13:31
  • Aha! The plot needs to be cleared between calls using `plt.clf()` as explained here https://stackoverflow.com/questions/20352111/why-does-matplotlib-savefig-images-overlap – dumbitdownjr Jun 15 '21 at 13:38