Editable legend in Dash's figures

For Dash figures, is it possible to have an editable legend in a figure, i.e. have the ability to drag by mouse the position of the legend, while maintaining the ability to filter/select specific plots in the figure by mouse clicks?

Thanks,

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