✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚡️ Concerned about the grid? Kyle Baranko teaches how to predicting peak loads using XGBoost. Register for the August webinar!

Markers disappear when moved by callback

Hello, everyone. Thanks for the great tool you’ve all created!

I’m building a rough schedule calendar based on a dcc.Graph. Calendar items are represented by shapes, rectangles in this case. Each shape has a corresponding marker, that will eventually be in “text” mode and will display a description of the calendar item. However, when I drag or resize a shape, its marker disappears.

Is this a bug or am doing something wrong?

Thanks in advance.

My imports:

import plotly.graph_objects as go

#import dash
import jupyter_dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input

import numpy as np

import json

Create figure

fig = go.Figure()

xrange = [-0.5, 7.5]
yrange = [6,18]

xwidth = abs(xrange[0] - xrange[1])
yheight = abs(yrange[0] - yrange[1])

# Set axes properties
fig.update_xaxes(range=[xrange[0], xrange[1]], showgrid=False)
fig.update_yaxes(range=[yrange[1], yrange[0]])

fig.update_layout(
    #autosize=False,
    showlegend = False
)

# Add shapes
fig.add_shape(
        go.layout.Shape(
            name = "blueShape",
            type="rect",
            xref = "x",
            yref = "y",
            x0=0,
            y0=7,
            x1=1,
            y1=8 - 0.1,            
            line=dict(
                width=0,
            ),
            fillcolor="skyBlue",
            opacity = 0.5,
            layer = "below"
        ))

fig.add_shape(
        # filled Rectangle
        go.layout.Shape(
            name = "pinkShape",
            type="rect",
            x0=0,
            y0=8,
            x1=1,
            y1=9.5 - 0.1,
            line=dict(
                width=0,
            ),
            fillcolor="Pink",
            opacity = 0.5,
            layer = "below"
        ))

fig.add_shape(
        # filled Rectangle
        go.layout.Shape(
            name = "greenShape",
            type="rect",
            x0=4,
            y0=15,
            x1=5,
            y1=15.5 - 0.1,
            line=dict(
                width=0,
            ),
            fillcolor="green",
            opacity = 0.5,
            layer = "below"
        ))

fig.add_shape(
        # filled Rectangle
        go.layout.Shape(
            name = "yellowShape",
            type="rect",
            x0=2,
            y0=13,
            x1=3,
            y1=14 - 0.1,
            line=dict(
                width=0,
            ),
            fillcolor="yellow",
            opacity = 0.5,
            layer = "below"
        ))

for shape in fig.layout.shapes:
    
    fig.add_trace(
        go.Scatter(
            x = [float(shape["x0"] + shape["x1"]) / 2],
            y = [float(shape["y0"] + shape["y1"]) / 2],
            visible = True,
            text = shape["name"],
            mode = "markers"))

fig.update_shapes(dict(xref='x', yref='y'))
#fig.show(config={'editable':True})

Layout and app

#app = dash.Dash()
app = jupyter_dash.JupyterDash()

app.layout = html.Div([
    
    dcc.Graph(id = 'chart',
              figure = fig,
              #config = {'editable': True, "edits":{"annotationPosition": False}}),
              config = {'editable': True}),
    html.Pre(id = "feedback"),
    html.Div(id = "store",
            #style={'display': 'none'}
            )
])

Callbacks

#snap the shapes to appropriate position
#output to "store" div
    #!! there is a dcc.Store object
@app.callback(Output("store", "children"),
              [Input("chart", "relayoutData"),
              Input("chart", "figure")])
def figToStore(feedback_data, fig):      
    
    if feedback_data != {"autosize": True}:
        
        shape = list(feedback_data.keys())[0][7]

        new_x0 = int(float(feedback_data["shapes[" + shape + "].x0"]) + 0.5)
        new_x1 = int(float(feedback_data["shapes[" + shape + "].x1"]) + 0.5)
        new_y0 = round(float(feedback_data["shapes[" + shape + "].y0"]) * 4) / 4
        new_y1 = round(float(feedback_data["shapes[" + shape + "].y1"] + 0.1) * 4) / 4 - 0.1# -0.1 to help differintiate between a block below

        if new_x0 == new_x1:
            new_x1 = new_x0 + 1

        if new_y0 == new_y1:
            new_y1 = new_y0 + 0.24

        fig["layout"]["shapes"][int(shape)]["x0"] = new_x0
        fig["layout"]["shapes"][int(shape)]["x1"] = new_x1
        fig["layout"]["shapes"][int(shape)]["y0"] = new_y0
        fig["layout"]["shapes"][int(shape)]["y1"] = new_y1
        
        #snap shape's marker<<<---------------------------------------------------<<<
        fig["data"][int(shape)]["x"] = (new_x0 + new_x1) / 2
        fig["data"][int(shape)]["y"] = (new_y0 + new_y1) / 2
        fig["data"][int(shape)]["visible"] = "true"

    return json.dumps(fig)

#placeholder, Input must not equal Output
@app.callback(Output("chart", "figure"),
             [Input("store", "children")])
def figToChart(fig):
    return json.loads(fig)

@app.callback(Output("feedback", "children"),
             [Input("chart", "relayoutData")])
def updatePre(feedback_data):
    return json.dumps(feedback_data)

Run server

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