Editable legend in Dash's figures

Hello @nlev,

Welcome to the community!

This is possible, please note, this uses javascript to perform the tasks:

app.py:

from dash import Input, Output, html, callback, Dash, dcc, State


app = Dash(__name__,
           external_stylesheets=['https://cdnjs.cloudflare.com/ajax/libs/dragula/3.6.6/dragula.css'],
           external_scripts=['https://cdnjs.cloudflare.com/ajax/libs/dragula/3.6.6/dragula.js',
                                       {'src':"https://code.jquery.com/jquery-3.6.3.min.js",
                                      'integrity':"sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=",
                                      'crossorigin':"anonymous"}])
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species",
                 size='petal_length', hover_data=['petal_width'])

fig.update_layout(uirevision=True)

app.layout = html.Div([
    html.Button('sync_figure', id='sync_figure'),
    dcc.Graph(figure=fig, id='testing'),
                       ])

app.clientside_callback(
    """function (id) {
        setTimeout(function () {
        $(".infolayer .legend").addClass('draggable')
        makeDraggable($(".infolayer .legend")[0])
        }, 300)
        return window.dash_clientside.no_update
    }""",
    Output('testing','id'), Input('testing','id')
)

app.clientside_callback(
    """
        function (n, fig) {
            if (n) {
                newFig = JSON.parse(JSON.stringify(fig))
                start = $("#testing .legend")[0].getBoundingClientRect()
                ref = $("#testing .bglayer")[0].getBoundingClientRect()
                newFig.layout['legend']['x'] = (start.left-ref.left)/ref.width
                newFig.layout['legend']['y'] = (1-(start.top-ref.top)/ref.height)
                return newFig
            }
            return window.dash_clientside.no_update
        }
    """,
    Output('testing','figure'),
    Input('sync_figure','n_clicks'),
    State('testing','figure')
)

app.run(debug=True)

assets/test.js:

function makeDraggable(ele) {
  var svg = ele.closest('.main-svg')

  function getMousePosition(evt) {
      var CTM = svg.getScreenCTM();
      return {
        x: (evt.clientX - CTM.e) / CTM.a,
        y: (evt.clientY - CTM.f) / CTM.d
      };
    }

  ele.addEventListener('mousedown', startDrag);
    var selectedElement, offset, transform;
    function startDrag(evt) {
      ele = evt.target.closest('.legend')
      ele.addEventListener('mousemove', drag);
      ele.addEventListener('mouseup', endDrag);
      ele.addEventListener('mouseleave', endDrag);
      if (ele.classList.contains('draggable')) {
        selectedElement = ele;
        offset = getMousePosition(evt);
        start = selectedElement.getAttributeNS(null, "transform").split('(')[1].split(')')[0]
        offset.x -= parseFloat(start.split(',')[0]);
        offset.y -= parseFloat(start.split(',')[1]);
      }
    }

    function drag(evt) {
      if (selectedElement) {
        evt.preventDefault();
        var coord = getMousePosition(evt);
        selectedElement.setAttributeNS(null, "transform", 'translate('+(coord.x - offset.x)+','+(coord.y - offset.y)+')');
      }
    }

    function endDrag(evt) {
      selectedElement = null;
      ele.removeEventListener('mousemove', drag);
      ele.removeEventListener('mouseup', endDrag);
      ele.removeEventListener('mouseleave', endDrag);
      $('#sync_figure').click()
    }
}

assets/test.css:

.draggable {
    cursor: move;
}

Currently, the sync_figure button is visible so you can see that it is there.

As you move the legend around and release, you may have some adjustments to the layout of your figure, as the legend is used during the calculation.

Make sure that you do not move the legend too fast, or it will stop moving if your cursor leaves it. :slight_smile:

1 Like