How to copy/paste shapes in a plot

Hi,
I have a go.Candlestick chart where I also let the user draw lines by using “fig.show(config={‘modeBarButtonsToAdd’:[‘drawline’]})”.
Is it possible for the user to copy/paste a line that has been drawn by the user so that we get two parallel lines? Then it would be possible for the user to grab one of the lines with the mouse and move it to visualize a trend channel in the chart.
Thanks

In Dash you could do this up to certain extend. I think I answered a similar question here, maybe you’ll find how to to it via the forum search.

Thanks, yes, sorry, it was a follow-up question in topic 72224, but I unfortunately didn’t manage to get that to work, but will give it another try.

I hope the Plotly developers some day will add button to copy/paste the selected shape, that you could add with ‘modeBarButtonsToAdd’, would be perfect :slight_smile:

As I said, that’s not too complicated to do in dash. If I find the time I’ll write a MRE.

Basically in a callback you extract the line coordinates, add an amount delta x or delta y to each and create a new line with the new coordinates. Then you append the new line to the shapes list in the figure layout and return the updated figure.

From dash 2.9 you could use the partial update for that so you would not have to return the complete figure.

I doubt this will ever find its way as built-in functionality because it’s a pretty specific thing. :hugs:

Here is a MRE for one possible approach. You could also use the relayoutData as input, analyse what changed and forget about the update button.

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

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
    x0 += distance
    x1 += distance
    return {
        'editable': False,
        'xref': 'x',
        'yref': 'y',
        'layer': 'above',
        'opacity': 1,
        'line':
            {
                'color': 'red',
                'width': 3,
                'dash': 'dash'
            },
        'type': 'line',
        'x0': x0,
        'y0': y0,
        'x1': x1,
        'y1': y1,
    }


# create figure
figure = go.Figure(
    data=go.Scatter(x=[1, 2, 3], y=[1, 2, 3], mode='markers+lines'),
    layout={
        'newshape': {
            'line': {
                'color': 'red',
                'width': 3,
            },
        }
    }
)

# add some buttons
graph_config = {
    'modeBarButtonsToAdd': [
        'drawline',
        'eraseshape'
    ]
}

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

        dcc.Graph(
            id='graph',
            figure=figure,
            config=graph_config
        )
    ]
)


@app.callback(
    Output('graph', 'figure'),
    Input('btn', 'n_clicks'),
    State('x_distance', 'value'),
    State('graph', 'figure'),
    prevent_initial_call=True
)
def update(_, x_distance, current_figure):
    # get master line
    master_line = current_figure['layout']['shapes'][0]

    # extract coordinates
    coordinates = extract_coordinates(master_line)

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

    # update figure
    current_figure['layout']['shapes'] = [master_line, slave_line]
    return current_figure


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

mred shapes

1 Like

Thanks so much!

I also figured out that if I want to keep other lines that I have drawn (instead of only the master_line and slave_line) I could change the row “current_figure[‘layout’][‘shapes’] = [master_line, slave_line]” to this:
current_figure[‘layout’][‘shapes’] += [slave_line]

A follow-up question if you don’t mind: Right now master_line is always set to current_figure[‘layout’][‘shapes’][0], but how can I change that so master_line is set to the currently selected line in the chart?

Hi @robin1,

this question came up quite often:

How to detect which shape is currently selected. There might be a way to do so in JS, but in python I don’t think this is possible because this happens clientside.