Issue with annotations example and relayoutData when modifying existing shape

Unless I am misunderstanding how modifying shapes change the RelayoutData dictionary, I think I’m having an issue: Some of the examples on the annotations page are no longer working (example, drawing a ROI and displaying the histogram: Image Annotations | Dash for Python Documentation | Plotly). I’m confused by the dictionary values of RelayoutData after modifying a shape.

In the final example (Image Annotations | Dash for Python Documentation | Plotly), when the shape is first made, the values shapes[0].x0, shapes[0].x1, shapes[0].y0, shapes[0].y1 exist within the shapes dictionary.

"shapes: [{
 'x0': 109.81480577256943,
 'y0': 195.22337962962962, 
 'x1': 240.92591688368054,
 'y1': 60.40856481481481
}]"

OK, this is how I expect the dictionary to be displayed and this is how most of the examples on the annotations page extract the coordinates. However, when I modify the shape, the relayoutData dictionary is no longer nested under shapes, but instead shows the modified shapes in the following format:

{
  "shapes[0].x0": 22.887654320987643,
  "shapes[0].x1": 349.05061728395054,
  "shapes[0].y0": 114.11214814814814,
  "shapes[0].y1": 426.368938271605
}

where there is now a string “shapes[0].x0” that has the updated coordinate value.

Any logic in a callback (such as the code shown below from the annotations example of plotting histogram from a ROI) will no longer work. Instead there would need to be a check for the string “shapes[0].x1” etc.

@callback(
    Output("histogram", "figure"),
    Input("graph-pic-camera", "relayoutData"),
    prevent_initial_call=True,
)
def on_new_annotation(relayout_data):
    if "shapes" in relayout_data:
        last_shape = relayout_data["shapes"][-1]
        # shape coordinates are floats, we need to convert to ints for slicing
        x0, y0 = int(last_shape["x0"]), int(last_shape["y0"])
        x1, y1 = int(last_shape["x1"]), int(last_shape["y1"])
        roi_img = img[y0:y1, x0:x1]
        return px.histogram(roi_img.ravel())
    else:
        return no_update

Is this a bug or is this the nature of the RelayoutData dictionary when modifying a shape? i.e. am I missing something here?

Many thanks for any help with understanding this, I hope my first post is not me mis-understanding something simple!

Richard

Embarrassingly, I may have solved the example issue (updating the histogram from ROI) as soon as I checked my post. In the example code, the rectangle must be drawn starting from one point and dragging to higher pixel values… I have a habit of drawing rectangle from bottom to top and that doesn’t work in the above example. Perhaps the example could be updated to account for this to make sure the roi_img sorts the y0 : y1 and x0 : x1 values from lowest to highest value!

Either way, I still have issue with the modified shape dictionary!

Hey @richardbriggs not sure if that is related, but we have an other using mentioning a difference in the returned values depending on the plotly version:

Hi @richardbriggs
Can you please share a minimal reproducible example so we can reproduce the error locally?

Hi @adamschroeder,

the final example on annotations page demonstrates this (the data.chelsea() cat image). If you create a rectangle and then modify the rectangle, the parsed RelayoutData dictionaries change in the way I described. I’m wondering w

import plotly.express as px
from dash import Dash, dcc, html, Input, Output, no_update, callback
from skimage import data
import json

img = data.chelsea()
fig = px.imshow(img)
fig.update_layout(dragmode="drawclosedpath")
config = {
    "modeBarButtonsToAdd": [
        "drawline",
        "drawopenpath",
        "drawclosedpath",
        "drawcircle",
        "drawrect",
        "eraseshape",
    ]
}

# Build App
app = Dash()
app.layout = html.Div(
    [
        html.H4("Draw a shape, then modify it"),
        dcc.Graph(id="fig-image", figure=fig, config=config),
        dcc.Markdown("Characteristics of shapes"),
        html.Pre(id="annotations-pre"),
    ]
)

@callback(
    Output("annotations-pre", "children"),
    Input("fig-image", "relayoutData"),
    prevent_initial_call=True,
)
def on_new_annotation(relayout_data):
    for key in relayout_data:
        if "shapes" in key:
            return json.dumps(f'{key}: {relayout_data[key]}', indent=2)
    return no_update

if __name__ == "__main__":
    app.run(debug=True)

I hadn’t noticed the code in the camera() image in the final example, which has a catch for the change in the parsed relayoutData. The elif any(["shapes" in key for key in relayout_data]): updates the coordinates based on the change in the dictionary.

@callback(
    Output("graph-hist", "figure"),
    Output("annotations", "children"),
    Input("fig-pic", "relayoutData"),
    prevent_initial_call=True,
)
def on_relayout(relayout_data):
    x0, y0, x1, y1 = (None,) * 4
    if "shapes" in relayout_data:
        last_shape = relayout_data["shapes"][-1]
        x0, y0 = int(last_shape["x0"]), int(last_shape["y0"])
        x1, y1 = int(last_shape["x1"]), int(last_shape["y1"])
        if x0 > x1:
            x0, x1 = x1, x0
        if y0 > y1:
            y0, y1 = y1, y0
    elif any(["shapes" in key for key in relayout_data]):
        x0 = int([relayout_data[key] for key in relayout_data if "x0" in key][0])
        x1 = int([relayout_data[key] for key in relayout_data if "x1" in key][0])
        y0 = int([relayout_data[key] for key in relayout_data if "y0" in key][0])
        y1 = int([relayout_data[key] for key in relayout_data if "y1" in key][0])
    if all((x0, y0, x1, y1)):
        roi_img = img[y0:y1, x0:x1]
        return (px.histogram(roi_img.ravel()), json.dumps(relayout_data, indent=2))
    else:
        return (no_update,) * 2

Wondering why relayoutData has this mechanism for displaying a detailed dictionary for new shapes, but only displays a snippet (formatted differently) after a modification?