sdbbs
sdbbs

Reputation: 5468

Jupyter notebook with detachable, floating and draggable widget?

Using Jupyter Notebook (v. 6.0.0) via Anaconda (Navigator v. 1.9.7) on Windows 10. Consider a situation where I have a ipywidgets slider on top, then a whole lotta text in between, then a graph on the botton that responds to the slider. Here is an image of such a webpage in Firefox, zoomed out so that it can be shown in full:

jupyter_long_output.png

Clearly, this is difficult to use - and what I would prefer in this case, is to "detach" the widget that contains the slider, and drag it down to approx where the graph is, then manipulate the slider; in that case, I also would not have to use the webpage rendering zoomed out. Then when done manipulating, I'd like to click a button, and have the widget reattached to where it was.

However, I am not sure, how could I implement this in Jupyter notebook with ipywidgets? The only thing close to this I've found is [Question] Maintain position of floating widget when browser tab is closed · Issue #2520 · jupyter-widgets/ipywidgets, which seems to imply that intervention in JavaScript is needed.

Here is the code to recreate the screenshot (there is already a "Detach" button in the widget containing the slider, except it does nothing else but change its text):

Cell 1 - Python 3:

import plotly.graph_objs as go
import numpy as np
from ipywidgets import widgets, Layout
from IPython.display import display
from IPython.display import Markdown as md

inSlider = widgets.FloatSlider(
    value=1.0,
    min=0.0,
    max=10.0,
    step=0.01,
    description='In:',
    continuous_update=True
)
output2 = widgets.Output()

def on_detButton_click(b):
    if b.description == "Detach": b.description = "Attach"
    else: b.description = "Detach"

detButton = widgets.Button(description="Detach")
detButton.on_click(on_detButton_click)

myctlwidget = widgets.VBox([widgets.HBox([inSlider, detButton]), output2])
display(myctlwidget)

Cell 2 is Markdown - pasted at end of this post;

Cell 3 - Python 3:

fig = go.FigureWidget( layout=go.Layout() )
fig.add_trace(go.Scatter(x=[0,10], y=[0,10],
                    mode='lines',
                    name='Test plot'))
# set range to prevent autoranging when slider sets a new value
fig.update_yaxes(range=[0, 11])

def update_graph(change):
    with fig.batch_update():
        curval = inSlider.value
        fig.data[0].y=[0, curval]

inSlider.observe(update_graph, names="value")

widgets.VBox([fig])

Cell 2 - Markdown (note two spaces at end of each first line, to introduce a line break)

Test paragraph 0  
Some text 0, 0.... some text 0

Test paragraph 1  
Some text 1, 1.... some text 1

Test paragraph 2  
Some text 2, 2.... some text 2

Test paragraph 3  
Some text 3, 3.... some text 3

Test paragraph 4  
Some text 4, 4.... some text 4

Test paragraph 5  
Some text 5, 5.... some text 5

Test paragraph 6  
Some text 6, 6.... some text 6

Test paragraph 7  
Some text 7, 7.... some text 7

Test paragraph 8  
Some text 8, 8.... some text 8

Test paragraph 9  
Some text 9, 9.... some text 9

Test paragraph 10  
Some text 10, 10.... some text 10

Test paragraph 11  
Some text 11, 11.... some text 11

Test paragraph 12  
Some text 12, 12.... some text 12

Test paragraph 13  
Some text 13, 13.... some text 13

Test paragraph 14  
Some text 14, 14.... some text 14

Test paragraph 15  
Some text 15, 15.... some text 15

Test paragraph 16  
Some text 16, 16.... some text 16

Test paragraph 17  
Some text 17, 17.... some text 17

Test paragraph 18  
Some text 18, 18.... some text 18

Test paragraph 19  
Some text 19, 19.... some text 19

Test paragraph 20  
Some text 20, 20.... some text 20

Test paragraph 21  
Some text 21, 21.... some text 21

Test paragraph 22  
Some text 22, 22.... some text 22

Test paragraph 23  
Some text 23, 23.... some text 23

Test paragraph 24  
Some text 24, 24.... some text 24

Test paragraph 25  
Some text 25, 25.... some text 25

Test paragraph 26  
Some text 26, 26.... some text 26

Test paragraph 27  
Some text 27, 27.... some text 27

Test paragraph 28  
Some text 28, 28.... some text 28

Test paragraph 29  
Some text 29, 29.... some text 29

Test paragraph 30  
Some text 30, 30.... some text 30

Upvotes: 2

Views: 2233

Answers (1)

sdbbs
sdbbs

Reputation: 5468

Ok, I think I got it working - thankfully, this version of Jupyter notebook automatically loads jquery-ui, so I can use .draggable() ...

Note that:

  • Having the whole background div as draggable, means that the whole element will be dragged even when you just want to move the slider head - so must use a drag handle instead (to allow for proper usage of the slider itself)
  • The detached/draggable element typically goes over other elements (as desired), except when in a cell which has a plotly diagram - once draggable element is released here (regardless if it happens over the plotly diagram itself, or other ipywidgets that may be present in the same cell output), the element cannot be clicked to be dragged anymore! (however, it can be click/dragged if the handle ends up in the two thin right and left borders)

If anyone comes up with a solution for the (second) problem, I'd love to hear it.

In the meantime, the workaround is to place a plain Markdown cell, below the cell output that contains the plotly diagram, which will then be able to properly "host" the dragged slider element. Here is how the fix looks like in this case (the little "(h)" is the handle):

jupyter-dragged-slider

Here is my fix:

Cell 1 - Python 3:

import plotly.graph_objs as go
import numpy as np
from ipywidgets import widgets, Layout
from IPython.display import display, Javascript
from IPython.display import Markdown as md

inSlider = widgets.FloatSlider(
    value=1.0,
    min=0.0,
    max=10.0,
    step=0.01,
    description='In:',
    continuous_update=True
)
output2 = widgets.Output()

def on_detButton_click(b):
    if b.description == "Detach": 
        b.description = "Attach"
        # must wrap in display() - Javascript() call on its own does not effectuate!
        # https://stackoverflow.com/questions/15193640/jquery-ui-draggable-reset-to-original-position
        # NOTE: in spite of zIndex manipulation: do NOT place dragged widget over textarea (it will lose focus),
        # also, it will be under a plot.ly diagram regardless!
        display(Javascript("""
        var $dragwidget = $("div.myctlwidget").parent();
        $dragwidget.data({
            'originalLeft': $dragwidget.css('left'),
            'originalTop': $dragwidget.css('top'),
            'originalZindex': $dragwidget.css('z-index')
        });
        $dragwidget.css({ 'z-index': 5000});
        $dragwidget.draggable({ disabled: false, handle: "div.myctlhandle" });
        """))
    else:
        b.description = "Detach"
        display(Javascript("""
        var $dragwidget = $("div.myctlwidget").parent();
        $dragwidget.draggable({ disabled: true });
        $dragwidget.css({
            'left': $dragwidget.data('originalLeft'),
            'top': $dragwidget.data('originalTop'),
            'z-index': $dragwidget.data('originalZindex'),
        });
        """))

detButton = widgets.Button(description="Detach")
detButton.on_click(on_detButton_click)

# NB: Button still is visually clickable, even when disabled :(
handleButton = widgets.Label("(h)", _dom_classes = ['myctlhandle'])

myctlwidget = widgets.VBox([widgets.HBox([inSlider, detButton, handleButton])], _dom_classes = ['myctlwidget'])
display(myctlwidget)
#myctlwidget._dom_classes = ['myctlwidget'] # nowork; only added as argument to VBox _dom_classes works!
#print(myctlwidget._dom_classes)

Cell 3 - Python 3:

fig = go.FigureWidget( layout=go.Layout() )
fig.add_trace(go.Scatter(x=[0,10], y=[0,10],
                    mode='lines',
                    name='Test plot'))
# set range to prevent autoranging
fig.update_yaxes(range=[0, 11])

def update_graph(change):
    with fig.batch_update():
        curval = inSlider.value
        fig.data[0].y=[0, curval]

inSlider.observe(update_graph, names="value")

#spacer = widgets.VBox([widgets.Text(":                     :"), widgets.Label(":                     :")]) # nope

display(md("Markdown here does NOT work as spacer!\n\n... does NOT work as spacer!"))
display(widgets.VBox([fig]))

Upvotes: 1

Related Questions