Plotly Dash combine dcc.Interval with dcc.Input

I have the following program that plots a line chart that develops each second with a new line segment at the end using dcc.Interval as well as a button using dcc.Input that duplicates a line that has been drawn in the chart by the user (thanks AIMPED!). The problem is that when I click “duplicate line” the line gets duplicated and visible in the chart for a second and then when dcc.Interval is triggered again all shapes disappears from the chart. How can I keep all shapes?

import dash
from dash import Input, Output, html, dcc, State
import plotly.graph_objects as go
from random import randrange

def extract_coordinates(line: dict) -> list:
    return [line.get(c) for c in ['x0', 'y0', 'x1', 'y1']]

def create_slave(coordinates: list, distance: float) -> dict:
    x0, y0, x1, y1 = coordinates
    y0 += distance
    y1 += distance
    return {'editable': True, 'xref': 'x', 'yref': 'y', 'layer': 'above', 'opacity': 1, 'line': {'color': 'green', 'width': 3, 'dash': 'solid'}, 'type': 'line', 'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1}

x_pos = [0]
y_pos = [0]

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(
            [
                dcc.Input(
                    id='y_distance',
                    type='number',
                    value=1.0,
                    step=0.1,
                ),
                html.Button('duplicate line', id='btn')
            ],
            style={'width': '10%'}
        ),

        dcc.Graph(
            id='graph',
            config={'modeBarButtonsToAdd': ['drawline','eraseshape']}
        ),

        dcc.Interval(
            id = 'graph-update',
            interval = 1000,
            n_intervals = 0
        )
    ]
)

@app.callback(
    Output('graph', 'figure'),
    Input('graph-update', 'n_intervals')
)
def update(n):
    global x_pos
    global y_pos
    x_pos += [x_pos[len(x_pos)-1]+1]
    y_pos += [y_pos[len(y_pos)-1]+randrange(-1,2)]

    fig = go.Figure(
        data=go.Scatter(x=x_pos, y=y_pos, mode='markers+lines'),
        layout = {'newshape': {'line': {'color': 'red', 'width': 3}}})

    fig.update_layout(dragmode='drawline')
    fig.update_layout(uirevision=True)

    return fig

@app.callback(
    Output('graph', 'figure', allow_duplicate=True),
    Input('btn', 'n_clicks'),
    State('y_distance', 'value'),
    State('graph', 'figure'),
    prevent_initial_call=True
)
def update(_, y_distance, current_figure):
    # get master line = the last line drawn
    master_line = current_figure['layout']['shapes'][len(current_figure['layout']['shapes'])-1]

    # extract coordinates
    coordinates = extract_coordinates(master_line)

    # create slave line in defined distance
    slave_line = create_slave(coordinates=coordinates, distance=y_distance)

    # update figure
    current_figure['layout']['shapes'] += [slave_line]

    return current_figure

if __name__ == '__main__':
    app.run(debug=True, port=8051)

Try including the uirevision=True directly in the layout when crating the fig instead of updating the layout. I had this weird problem in the past that it made a difference how you do this.

Thanks for the suggestion! I tried that, but it didn’t help in this case, diff before/after:

$ diff /tmp/before.py /tmp/after.py
59,62c59
<         layout = {'newshape': {'line': {'color': 'red', 'width': 3}}})
< 
<     fig.update_layout(dragmode='drawline')
<     fig.update_layout(uirevision=True)
---
>         layout = {'newshape': {'line': {'color': 'red', 'width': 3}}, 'dragmode': 'drawline', 'uirevision': True})

Instead I found a solution by creating the figure first and then have both the callback using dcc.Interval and the callback using dcc.Input both just updating the current_figure. Since I am fairly new at using dash I for sure would appreciate any feedback on how to make the code below better in some way or more resource efficient. In my real world usage I have to process about 1,000 incoming new datapoints per minute which I plan to do in a separate thread where a parallel thread is updating the chart once each second. In this example I also mark the top and the bottom with a horizontal line:

import dash
from dash import Input, Output, html, dcc, State
import plotly.graph_objects as go
from random import randrange

x_pos = [0]
y_pos = [0]

figure = go.Figure(
    data = go.Scatter(x = x_pos, y = y_pos, mode = 'lines', line = dict(color="#0000ff")),
    layout = {'newshape': {'line': {'color': 'red', 'width': 3}}, 'dragmode': 'drawline', 'uirevision': True}
)

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(
            [
                dcc.Input(
                    id = 'y_distance',
                    type = 'number',
                    value = 1.0,
                    step = 0.1,
                ),
                html.Button('duplicate line', id='btn')
            ],
            style={'width': '10%'}
        ),

        dcc.Graph(
            id = 'graph',
            figure = figure,
            config = {'modeBarButtonsToAdd': ['drawline', 'eraseshape']}
        ),

        dcc.Interval(
            id = 'graph-update',
            interval = 1000,
            n_intervals = 0
        )
    ]
)

@app.callback(
    Output('graph', 'figure'),
    Input('graph-update', 'n_intervals'),
    State('graph', 'figure')
)
def update(n, current_figure):
    global x_pos
    global y_pos

    # Add new segment to line chart [blue]
    x_pos += [x_pos[len(x_pos)-1] + 1]
    y_pos += [y_pos[len(y_pos)-1] + randrange(-1,2)]
    current_figure['data'] = [{'line': {'color': '#0000ff'}, 'mode': 'lines', 'x': x_pos, 'y': y_pos, 'type': 'scatter'}]

    # Mark high with horiz line [red]
    x0, y0, x1, y1 = [min(x_pos), max(y_pos), max(x_pos), max(y_pos)]
    current_figure['data'] += [{'marker': {'color': '#ff0000'}, 'mode': 'lines', 'x': [x0, x1], 'y': [y0, y1], 'type': 'scatter'}]

    # Mark low with horiz line [green]
    x0, y0, x1, y1 = [min(x_pos), min(y_pos), max(x_pos), min(y_pos)]
    current_figure['data'] += [{'marker': {'color': '#00ff00'}, 'mode': 'lines', 'x': [x0, x1], 'y': [y0, y1], 'type': 'scatter'}]

    return current_figure

@app.callback(
    Output('graph', 'figure', allow_duplicate=True),
    Input('btn', 'n_clicks'),
    State('y_distance', 'value'),
    State('graph', 'figure'),
    prevent_initial_call = True
)
def update(_, y_distance, current_figure):
    # Last line drawn by user
    last_line = current_figure['layout']['shapes'][len(current_figure['layout']['shapes'])-1]

    # Create duplicate of last line
    x0, y0, x1, y1 = [last_line.get(c) for c in ['x0', 'y0', 'x1', 'y1']]
    y0 += y_distance
    y1 += y_distance
    duplicate_line = {'editable': True, 'xref': 'x', 'yref': 'y', 'layer': 'above', 'opacity': 1, 'line': {'color': 'black', 'width': 3, 'dash': 'solid'}, 'type': 'line', 'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1}

    # Add duplicate line to chart
    current_figure['layout']['shapes'] += [duplicate_line]

    return current_figure

if __name__ == '__main__':
    app.run(debug=True, port=8051)