Mouse interactivity within subplots (resize - reorder)

Hello @popo,

It took me some time, but I finally got something I think you’ll be happy with. :stuck_out_tongue: You can polish it however you want to, this is just a rough showing.

dragging-subplots

All of this is done on the client side, however… You can use the clientside to chain a callback to the server, easily.

Here is the .js file:

function coolEffects() {
    $(".modebar-group:first-of-type").prepend($('<button onclick=toggleEdit(this)>Testing</button>'))
}

var sp;
var oldDom;

function toggleEdit(el) {
    if (!$(el).closest('.dash-graph').hasClass('editing')) {
        plots = []
        jQuery.fn.reverse = [].reverse;
        function SortByTop(a, b){
              var top1 = a.getBoundingClientRect().top;
              var top2 = b.getBoundingClientRect().top;
              return ((top1 < top2) ? -1 : ((top1 > top2) ? 1 : 0));
            }
        $(el).closest('.dash-graph').find('.draglayer > g').sort(SortByTop).each(function (index) {
            plots.push($('<div class="dragula">' + this.classList + '</div>'))
            var parentPos = $(this).parent()[0].getBoundingClientRect(),
            childPos = this.getBoundingClientRect(),
            relativePos = {};

            relativePos.top = childPos.top - parentPos.top,
            relativePos.right = childPos.right - parentPos.right,
            relativePos.bottom = childPos.bottom - parentPos.bottom,
            relativePos.left = childPos.left - parentPos.left;


            $(plots[plots.length -1]).css(
                {'top': relativePos.top,
                'left': relativePos.left,
                'height': childPos.height,
                'width': childPos.width,
            })
        })

        $($(el).closest('.dash-graph').find('div')[0]).append(plots)
        drake = dragula([$(el).closest('.dash-graph').find('div')[0]])
        $('.dragula').each(function(index) { if (index != $('.dragula').length-1) {
            $(this).after($('<div class="divider"><div class="fa fa-bars"></div></div>'))}}
        )
        $(".divider").each(function () {
            dragElement(this)
        })
        drake.on('drop', function() {
            $('.divider').remove()
            $('.dragula').each(function(index) { if (index < $('.dragula').length-2) {
            $(this).after($('<div class="divider"><div class="fa fa-bars"></div></div>'))}})
            $(".divider").each(function () {
                dragElement(this)
            })
        })

    } else {
        $('#swap').click()
    }
    setTimeout(function () {
    $(el).closest('.dash-graph').toggleClass('editing')

    }, 1
    )
}

function dragElement(elmnt) {
  var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  elmnt.onmousedown = dragMouseDown;

  function dragMouseDown(e) {
    e = e || window.event;
    e.preventDefault();
    // get the mouse cursor position at startup:
    pos4 = e.clientY;
    document.onmouseup = closeDragElement;
    // call a function whenever the cursor moves:
    document.onmousemove = elementDrag;
  }

  function elementDrag(e) {
    e = e || window.event;
    e.preventDefault();
    pos2 = pos4 - e.clientY;
    pos4 = e.clientY;
    $(elmnt).prev()[0].style.height = parseFloat($(elmnt).prev()[0].style.height) - pos2 + 'px'
    $(elmnt).next()[0].style.height = parseFloat($(elmnt).next()[0].style.height) + pos2 + 'px'
  }

  function closeDragElement() {
    // stop moving when mouse button is released:
    document.onmouseup = null;
    document.onmousemove = null;
  }
}


window.fetch = new Proxy(window.fetch, {
    apply(fetch, that, args) {
        // Forward function call to the original fetch
        const result = fetch.apply(that, args);

        // Do whatever you want with the resulting Promise
        result.then((response) => {
            if (args[0] == '/_dash-update-component') {
                setTimeout(function() {coolEffects()}, 1000)
            }})
        return result
        }
    }
    )

$(document).ready(function() {
    setTimeout(function() {coolEffects()}, 1000)
})

Here is the app file:

import dash
from dash import dcc, html, Input, Output, State
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import dash_bootstrap_components as dbc
from pprint import pprint

fig = make_subplots(rows=3, cols=1)

fig.add_trace(
    go.Scatter(x=[1, 2, 3], y=[4, 5, 6]),
    row=1, col=1
)

fig.add_trace(
    go.Scatter(x=[100, 200, 300], y=[400, 500, 600]),
    row=3, col=1
)

fig.add_trace(
    go.Scatter(x=[20, 30, 40], y=[50, 60, 70]),
    row=2, col=1
)
fig.update_layout(height=600, width=800, title_text="MRE Subplots")

app = dash.Dash(__name__,
                external_stylesheets = [dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME,
    "https://epsi95.github.io/dash-draggable-css-scipt/dragula.css"],
                external_scripts=[{'src':"https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"},
                                 "https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js",
                               "https://epsi95.github.io/dash-draggable-css-scipt/script.js"
])

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

app.clientside_callback(
    """
        function (n, f) {
            newFig = JSON.parse(JSON.stringify(f))
            console.log(f.layout)
            dragEls = $('.dragula')
            
            l = $('.editing .draglayer > g')
            if (!sp) {
                sp = Math.abs(f.layout.yaxis.domain[0] - f.layout.yaxis2.domain[1])
            }
            
            if (dragEls[0]) {
                newLayout = JSON.parse(JSON.stringify(f.layout))
                ht = 0
                try {
                    $('.dragula').each( function () { ht += parseFloat(this.style.height)})
                    ht += sp*(dragEls.length-1)
                } catch {}
                for (y=0; y<dragEls.length; y++) {
                    per = parseFloat(dragEls[y].style.height) / ht
                    
                    if ($(dragEls[y]).text() == 'xy') {
                        ref = ''
                    } else {
                        ref = $(dragEls[y]).text().split('y')[0].split('x')[1]
                    }
                    if (y==0) {
                        newLayout['yaxis'+ref] = f.layout['yaxis'+ref]
                        newLayout['yaxis'+ref].domain = [1-per+(sp/2), 1]
                        oldDom = 1 - per
                    } 
                    else {
                        newLayout['yaxis'+ref] = f.layout['yaxis'+ref]
                        if ((oldDom - per+(sp/2)) < 0) {
                            newLayout['yaxis'+ref].domain = [0, oldDom]
                        } else {
                            newLayout['yaxis'+ref].domain = [oldDom - per+(sp/2), oldDom]
                        }
                        oldDom = oldDom - per
                        
                    }
                }
                newFig.layout = newLayout
                try {
                    setTimeout(function() {
                    drake.destroy()}, 500)
                } catch {}
                $($('.editing').find('div')[0]).empty()
                console.log(newLayout)
            }
            return JSON.parse(JSON.stringify(newFig))
        }
    """,
    Output('myFig','figure'),
    Input("swap", "n_clicks"),
    State('myFig', 'figure'),
    prevent_intial_call=True
)

app.run_server(debug=True)

css file:


.editing .main-svg {
    visibility: hidden;
}

.editing > div:first-child {
    position: absolute;
    height: 100%;
    width: 100%;
    z-index: 1;
}

.editing .modebar-container {
    z-index: 2;
}

.dragula {
    background-color: silver;
    color: white;
    font-size: 20pt;
    font-weight: heavy;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1pt dashed black;
    margin: 2px;
    cursor: grab;
}

.divider {
    box-shadow: inset 0px 0px 5px silver;
    height: 10px;
    width: 100%;
    cursor: grab;
    display: flex;
    align-items: center;
    justify-content: center;
}

Unfortunately, with this style of callback , you wont be able to individually update the figure if needed from the server, other than through controlling all the children of the page, or what not.

5 Likes