How to add scatter plots to single animation frames of imshow

Hey there,
I am trying to develop a debugging tool where I have a series of background images as an animation. Now I’d like to add differing traces to each of the displayed images (= the single frames). If I add the traces to the complete figure, this doesn’t work cause they are displayed for the whole length of the animation then but I only need them to show on one time step of the animation. But I couldn’t find a function where I can add a trace to a single frame.
I found solutions to combine scatters and bars plots but it doesn’t work for images because of the dimensions.

Preferably I’d use to dash in the longer run but also displaying it just in a jupyter would do the job!

Any help would be greatly appreciated! Thanks :))

If this is not possible it’d be very helpful to know whether it is possible have two subplots (one scatter and one imshow!) in the same animation? Again I could only find combinations of bars and scatters but nothing with images and some other plots…
Thanks!

Hello @Vale1,

Welcome to the community!

I do not think that there is a way to add the annotations to a single frame without using dash callbacks.

If you start using dash and dash Jupyter, then you can update the figures layout to include the annotations at different frames.

Thanks for your answer! I am working with callbacks. However, I am not sure how I can combine plotting data with the series of background images. Do you have any advice for me on how to do that?
This is my current code (it displays the data and image information next to each other but not combined in one graph…

from dash import Dash, html, dcc, Input, Output
import pandas as pd
import plotly.express as px
import cv2
import glob
import numpy as np

external_stylesheets = [‘https://codepen.io/chriddyp/pen/bWLwgP.css’]

app = Dash(name, external_stylesheets=external_stylesheets)

df = pd.read_csv(‘info.csv’, delimiter=“;”)

read images

file = “plots/*.png”
glob.glob(file)
img=
for i in range(20):
image = “{}.png”.format(i)
img.append(cv2.imread(image))
img = np.asarray(img, dtype=np.uint8)

fig = px.imshow(img,origin=“upper”, animation_frame=0, binary_string=True, labels=dict(animation_frame=“slice”))
fig.update_layout(width=500, height=500)

pass just the function name to apply

app.layout = html.Div([
html.Div([

    html.Div([
        dcc.Dropdown(
            df['horizon'].unique(),                     # used to be inficator name
            2,
            id='crossfilter-xaxis-column',
        ),
        dcc.RadioItems(
            ['Linear', 'Log'],
            'Linear',
            id='crossfilter-xaxis-type',
            labelStyle={'display': 'inline-block', 'marginTop': '5px'}
        )
    ],
    style={'width': '49%', 'display': 'inline-block'}),
    html.Div([
        dcc.Dropdown(
            df['dt'].unique(),
            0.1,
            id='crossfilter-yaxis-column'
        ),
        dcc.RadioItems(
            ['Linear', 'Log'],
            'Linear',
            id='crossfilter-yaxis-type',
            labelStyle={'display': 'inline-block', 'marginTop': '5px'}
        )
    ], style={'width': '49%', 'float': 'right', 'display': 'inline-block'})
], style={
    'padding': '10px 5px'
}),

html.Div([
    dcc.Graph(
        id='crossfilter-indicator-scatter',
        hoverData={'points': [{'customdata': '0'}]}
    )
], style={'width': '49%', 'display': 'inline-block', 'padding': '0 20'}),


html.Div([
    dcc.Graph(figure=fig),
], style={'display': 'inline-block', 'width': '45%'}),


html.Div(dcc.Slider(
    df['time_step'].min(),
    df['time_step'].max(),
    step=None,
    id='crossfilter-time_step--slider',
    value=df['time_step'].max(),
    marks={str(time_step): str(time_step) for time_step in df['time_step'].unique()}
), style={'width': '49%', 'padding': '0px 20px 20px 20px'}),


html.Div([
    dcc.Graph(id='x-time-series'),
    dcc.Graph(id='y-time-series'),
], style={'display': 'inline-block', 'width': '49%'}),

])

@app.callback(
Output(‘crossfilter-indicator-scatter’, ‘figure’),
Input(‘crossfilter-xaxis-column’, ‘value’),
Input(‘crossfilter-yaxis-column’, ‘value’),
Input(‘crossfilter-xaxis-type’, ‘value’),
Input(‘crossfilter-yaxis-type’, ‘value’),
Input(‘crossfilter-time_step–slider’, ‘value’))
def update_graph(xaxis_column_name, yaxis_column_name,
xaxis_type, yaxis_type,
time_step_value):
dff = df[df[‘time_step’] == time_step_value]

fig = px.scatter(x=dff[dff['horizon'] == xaxis_column_name]['x_position_m'], # to be changed
        y=dff[dff['dt'] == yaxis_column_name]['y_position_m']#, # to be changed
        #hover_name = dff[dff['dt'] == yaxis_column_name]['trajectory_number']
        )

fig.update_traces(customdata=dff[dff['dt'] == yaxis_column_name]['trajectory_number'])

fig.update_xaxes(title=xaxis_column_name, type='linear' if xaxis_type == 'Linear' else 'log')

fig.update_yaxes(title=yaxis_column_name, type='linear' if yaxis_type == 'Linear' else 'log')

fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')

return fig

def create_time_series(dff, axis_type, title):

fig = px.scatter(dff, x='time_step', y='x_position_m') # to be changed

fig.update_traces(mode='lines+markers')

fig.update_xaxes(showgrid=False)

fig.update_yaxes(type='linear' if axis_type == 'Linear' else 'log')

fig.add_annotation(x=0, y=0.85, xanchor='left', yanchor='bottom',
                   xref='paper', yref='paper', showarrow=False, align='left',
                   text=title)

fig.update_layout(height=225, margin={'l': 20, 'b': 30, 'r': 10, 't': 10})

return fig

@app.callback(
Output(‘x-time-series’, ‘figure’),
Input(‘crossfilter-indicator-scatter’, ‘hoverData’),
Input(‘crossfilter-xaxis-column’, ‘value’),
Input(‘crossfilter-xaxis-type’, ‘value’))
def update_y_timeseries(hoverData, xaxis_column_name, axis_type):
traj_name = hoverData[‘points’][0][‘customdata’]
dff = df[df[‘trajectory_number’] == traj_name]
dff = dff[dff[‘horizon’] == xaxis_column_name]
title = ‘{}
{}’.format(traj_name, xaxis_column_name)
return create_time_series(dff, axis_type, title)

@app.callback(
Output(‘y-time-series’, ‘figure’),
Input(‘crossfilter-indicator-scatter’, ‘hoverData’),
Input(‘crossfilter-yaxis-column’, ‘value’),
Input(‘crossfilter-yaxis-type’, ‘value’))
def update_x_timeseries(hoverData, yaxis_column_name, axis_type):
dff = df[df[‘trajectory_number’] == hoverData[‘points’][0][‘customdata’]]
dff = dff[dff[‘dt’] == yaxis_column_name]
return create_time_series(dff, axis_type, yaxis_column_name)

if name == ‘main’:
app.run_server(debug=True)

@Vale1,

Could you try to format your code a lite better using the tools on this site. It will make it easier to read and also copy.

Sorry and thanks for the instructions! I also removed some additional things I was plotting so the only thing are the two “windows” which I’d like to combine (the scatter which can be iterated by a slider and the animation of the background images). They have the same number of frames so optimally I’m trying to figure out a solution in which they share a slider.

from dash import Dash, html, dcc, Input, Output
import pandas as pd
import plotly.express as px
import cv2
import glob
import numpy as np

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

# saved information
df = pd.read_csv('info.csv', delimiter=";")

# read images
file = "plots/*.png"
glob.glob(file)
img=[]
for i in range(20):
    image = "{}.png".format(i)
    img.append(cv2.imread(image))
img = np.asarray(img, dtype=np.uint8) 

fig = px.imshow(img,origin="upper", animation_frame=0, binary_string=True, labels=dict(animation_frame="slice"))
fig.update_layout(width=500, height=500)

# pass just the function name to apply
app.layout = html.Div([
    html.Div([

        html.Div([
            dcc.Dropdown(
                df['horizon'].unique(),                     # used to be inficator name
                2,
                id='crossfilter-xaxis-column',
            ),
            dcc.RadioItems(
                ['Linear', 'Log'],
                'Linear',
                id='crossfilter-xaxis-type',
                labelStyle={'display': 'inline-block', 'marginTop': '5px'}
            )
        ],
        style={'width': '49%', 'display': 'inline-block'}),
        html.Div([
            dcc.Dropdown(
                df['dt'].unique(),
                0.1,
                id='crossfilter-yaxis-column'
            ),
            dcc.RadioItems(
                ['Linear', 'Log'],
                'Linear',
                id='crossfilter-yaxis-type',
                labelStyle={'display': 'inline-block', 'marginTop': '5px'}
            )
        ], style={'width': '49%', 'float': 'right', 'display': 'inline-block'})
    ], style={
        'padding': '10px 5px'
    }),

    html.Div([
        dcc.Graph(
            id='crossfilter-indicator-scatter',
            hoverData={'points': [{'customdata': '0'}]}
        )
    ], style={'width': '49%', 'display': 'inline-block', 'padding': '0 20'}),


    html.Div([
        dcc.Graph(figure=fig),
    ], style={'display': 'inline-block', 'width': '45%'}),


    html.Div(dcc.Slider(
        df['time_step'].min(),
        df['time_step'].max(),
        step=None,
        id='crossfilter-time_step--slider',
        value=df['time_step'].max(),
        marks={str(time_step): str(time_step) for time_step in df['time_step'].unique()}
    ), style={'width': '49%', 'padding': '0px 20px 20px 20px'}),
])


@app.callback(
    Output('crossfilter-indicator-scatter', 'figure'),
    Input('crossfilter-xaxis-column', 'value'),
    Input('crossfilter-yaxis-column', 'value'),
    Input('crossfilter-xaxis-type', 'value'),
    Input('crossfilter-yaxis-type', 'value'),
    Input('crossfilter-time_step--slider', 'value'))
def update_graph(xaxis_column_name, yaxis_column_name,
                 xaxis_type, yaxis_type,
                 time_step_value):
    dff = df[df['time_step'] == time_step_value]

    fig = px.scatter(x=dff[dff['horizon'] == xaxis_column_name]['x_position_m'], # to be changed
            y=dff[dff['dt'] == yaxis_column_name]['y_position_m']#, # to be changed
            #hover_name = dff[dff['dt'] == yaxis_column_name]['trajectory_number']
            )

    fig.update_traces(customdata=dff[dff['dt'] == yaxis_column_name]['trajectory_number'])

    fig.update_xaxes(title=xaxis_column_name, type='linear' if xaxis_type == 'Linear' else 'log')

    fig.update_yaxes(title=yaxis_column_name, type='linear' if yaxis_type == 'Linear' else 'log')

    fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')

    return fig

if __name__ == '__main__':
    app.run_server(debug=True)

Thanks for taking the time! It’s very much appreciated :))

@Vale1,

Do you have any set of the data that we could use to test out? (“info.csv”)

Thanks for your reply!
The idea of the info.csv is very simple. Just for each of the time_steps ( which = number of images = number of frames of the image animation) there are some lines (each has a unique id) and is saved by two lists (x_coordinates and y_coordiantes) which can be zipped to get the points of the line.
This could look like the following:

So the goal is to plot these lines on the corresponding frame (frame is determined by the timestep) in the image animation (optimally displaying the unique id as additional information).
If it is more easy to only plot points that would also do the job and the unique id is also not that important :slight_smile:

@jinnyzor sorry for bothering, do you know whether this is possible?

HI @Vale1,

I do not really understand what you are trying to do or what exactly the issue is. Maybe you could:

  • add a gif or some images that show the current status and the issue you have
  • strip down your code even more (without styling, log axis…)
  • share your data in a way that it is usable directly (csv and some png files on google drive, or similar)

Hello @Vale1,

Sorry about the delay.

Have you tried setting the animation_frame = ‘time_step’? I think this is what you are after.

Hello @jinnyzor, Hello @AIMPED ,

thank you for your answers. What I would like to achieve is to add the information (x_position_m and y_position_m) to an already existing figure. Namely the following:

fig = px.imshow(img,origin="upper", animation_frame=0, binary_string=True, labels=dict(animation_frame="slice"))

This figure is an animation showing a bunch of images (one per frame). On each image I would like to “mark” some areas of interest (the position of those areas of interest is saved as dots consisting out of x_position_m[i] and y_position_m[i].
So the idea is rather simple, I would like to plot some dots on each image/frame.