How can I make shapes in figure un-resizable but still movable?

I have an image displayed as a figure in Graph. I added a rectangle shape onto the figure. I want the rectangle to be movable but disable resizing it. Additionally, I’d like to hide the resize cursor and show the move cursor when hovering over the rectangle.

How do I achieve this? I’ve tried setting config={'edits': {'shapePosition': True}}, but it allows both moving and resizing.

hi @DhiraPT
Can you please show us a minimal reproducible example (MRE) – code that you’ve written so far with a rectangle shape on a graph?

Hi @adamschroeder

import dash
from dash import dcc, html
import plotly.graph_objects as go


app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Graph(
        id='my-graph',
        figure=go.Figure({
            'layout': {
                'shapes': [
                    {
                        'type': 'rect',
                        'x0': 0,
                        'y0': 0,
                        'x1': 2,
                        'y1': 2,
                        'line': {
                            'color': 'RoyalBlue',
                        },
                    },
                ],
            },
        }),
        style={'width': '100%', 'height': '600px'},
        config={'edits': {'shapePosition': True}},
    )
])


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

I want the rectangle to only be movable, but not resizable.

hi @DhiraPT
I don’t believe that’s possible out of the box.
As we see from this chapter, shapes can be pre-drawn or drawn by the user. And if you click on the shape’s frame you would be able to move it around, but also resize it (which I know you don’t want).

You could potentially use Dash to allow user to redefine the shape location by choosing the:
x0=..., x1=..., y0=..., y1=... But that wouldn’t be using the move cursor unfortunately.

Hi @adamschroeder

I found a solution in Plotly.js by creating a rect element and appending it as a child to the svg element inside the Plotly object. I then added event listeners for mousedown, mouseup, and mousemove to calculate the new position, taking into consideration the zoom level to maintain the size of the rectangle, and subsequently relayout the Plotly object each time.

It’s quite tedious. I’m trying to incorporate the algorithm into Plotly Python.

1 Like

quite creative. Do you mind sharing your solution code here so other people can learn from you?

Sure! Here’s the code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Plotly Draggable Rectangle</title>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <style>
        #custom-plot {
            width: 800px;
            height: 800px;
        }
    </style>
</head>
<body>
    <div id="custom-plot"></div>
    <script>
        // Initial rect coordinates in figure
        const initialRect = { x0: 1, y0: 1, x1: 2, y1: 2 };

        // Current rect coordinates in figure
        let currentRect = { ...initialRect };

        const data = [
            {
                x: [1, 2, 3, 4],
                y: [1, 2, 3, 4],
                mode: 'markers',
                type: 'scatter'
            }
        ];

        const layout = {
            dragmode: 'zoom',
            xaxis: {
                scaleanchor: 'y',
                scaleratio: 1
            },
            yaxis: {
                scaleanchor: 'x',
                scaleratio: 1
            }
        };

        // Create the Plotly figure
        Plotly.newPlot('custom-plot', data, layout).then(plot => {
            // Create the rect
            const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
            rect.setAttribute('fill', 'none');
            rect.setAttribute('stroke', 'red');
            rect.setAttribute('stroke-width', 2);
            rect.style.cursor = 'move';
            rect.style.pointerEvents = 'all';

            // Add rect to the plot
            plot.querySelector('svg').appendChild(rect);

            let isDragging = false;
            let offsetX, offsetY;

            function updateRect(x0, y0, x1, y1) {
                const xScale = plot._fullLayout.xaxis._length / (plot._fullLayout.xaxis.range[1] - plot._fullLayout.xaxis.range[0]);
                const yScale = plot._fullLayout.yaxis._length / (plot._fullLayout.yaxis.range[1] - plot._fullLayout.yaxis.range[0]);

                const x = (x0 - plot._fullLayout.xaxis.range[0]) * xScale + plot._fullLayout.margin.l;
                const y = plot._fullLayout.height - ((y1 - plot._fullLayout.yaxis.range[0]) * yScale + plot._fullLayout.margin.b);
                const width = (x1 - x0) * xScale;
                const height = (y1 - y0) * yScale;

                rect.setAttribute('x', x);
                rect.setAttribute('y', y);
                rect.setAttribute('width', width);
                rect.setAttribute('height', height);
            }

            updateRect(currentRect.x0, currentRect.y0, currentRect.x1, currentRect.y1);

            rect.addEventListener('mousedown', function (e) {
                isDragging = true;
                offsetX = e.clientX - parseFloat(rect.getAttribute('x'));
                offsetY = e.clientY - parseFloat(rect.getAttribute('y'));
            });

            document.addEventListener('mousemove', function (e) {
                if (isDragging) {
                    const x = e.clientX - offsetX;
                    const y = e.clientY - offsetY;
                    const x0 = plot._fullLayout.xaxis.range[0] + (x - plot._fullLayout.margin.l) / plot._fullLayout.xaxis._length * (plot._fullLayout.xaxis.range[1] - plot._fullLayout.xaxis.range[0]);
                    const y1 = plot._fullLayout.yaxis.range[0] + (plot._fullLayout.height - y - plot._fullLayout.margin.b) / plot._fullLayout.yaxis._length * (plot._fullLayout.yaxis.range[1] - plot._fullLayout.yaxis.range[0]);
                    const width = parseFloat(rect.getAttribute('width'));
                    const height = parseFloat(rect.getAttribute('height'));

                    currentRect = {
                        x0: x0,
                        y0: y1 - height / plot._fullLayout.yaxis._length * (plot._fullLayout.yaxis.range[1] - plot._fullLayout.yaxis.range[0]),
                        x1: x0 + width / plot._fullLayout.xaxis._length * (plot._fullLayout.xaxis.range[1] - plot._fullLayout.xaxis.range[0]),
                        y1: y1
                    };

                    updateRect(currentRect.x0, currentRect.y0, currentRect.x1, currentRect.y1);
                }
            });

            document.addEventListener('mouseup', function () {
                isDragging = false;
            });

            plot.on('plotly_relayout', function () {
                updateRect(currentRect.x0, currentRect.y0, currentRect.x1, currentRect.y1);
            });
        });
    </script>
</body>
</html>
1 Like