0

I'm trying to label a pandas-df (containing timeseries data) with the help of a bokeh-lineplot, box_select tool and a TextInput widget in a jupyter-notebook. How can I access the by the box_select selected data points?

I tried to edit a similar problems code (Get selected data contained within box select tool in Bokeh) by changing the CustomJS to something like:

source.callback = CustomJS(args=dict(p=p), code="""
        var inds = cb_obj.get('selected')['1d'].indices;
        [source.data['xvals'][i] for i in inds] = 'b'
        """
)

but couldn't apply a change on the source of the selected points.

So the shortterm goal is to manipulate a specific column of source of the selected points.

Longterm I want to use a TextInput widget to label the selected points by the supplied Textinput. That would look like:

enter image description here

EDIT:

That's the current code I'm trying in the notebook, to reconstruct the issue:

from random import random

import bokeh as bk
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, HoverTool
from bokeh.plotting import figure, output_file, show, output_notebook

output_notebook()

x = [random() for x in range(20)]
y = [random() for y in range(20)]

hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "@label")])

source = ColumnDataSource(data=dict(x=x, y=y, label=[i for i in "a"*20]))
p1 = figure(plot_width=400, plot_height=400, tools="box_select", title="Select Here")
p1.circle('x', 'y', source=source, alpha=0.6)
p1.add_tools(hovertool)
source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
        var inds = cb_obj.indices;
        for (var i = 0; i < inds.length; i++) {
            source.data['label'][inds[i]] = 'b'
        }
        source.change.emit();
    """)
)

layout = row(p1)

show(layout)
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Andre S.
  • 478
  • 4
  • 13

2 Answers2

1

The main thing to note is that BokehJS can only automatically notice updates when actual assignments are made, e.g.

source.data = some_new_data

That would trigger an update. If you update the data "in place" then BokehJS is not able to notice that. You will have to be explicit and call source.change.emit() to let BokehJS know something has been updated.

However, you should also know that you are using three different things that are long-deprecated and will be removed in the release after next.

  • cb_obj.get('selected')

    There is no need to ever use .get You can just access properties directly:

    cb_obj.selected
    
  • The ['1d'] syntax. This dict approach was very clumsy and will be removed very soon. For most selections you want the indices property of the selection:

    source.selected.indices
    
  • source.callback

    This is an ancient ad-hoc callback. There is a newer general mechanism for callbacks on properties that should always be used instead

    source.selected.js_on_change('indices', CustomJS(...))
    

    Note that in this case, the cb_obj is the selection, not the data source.

bigreddot
  • 33,642
  • 5
  • 69
  • 122
  • Thanks @bigreddot! I changed the code to: ```source.selected.js_on_change('indices',CustomJS(args=dict(source=source), code=""" var inds = cb_obj.indices; for (var i = 0; i < inds.length; i++) { source.data['label'][inds[i]] = 'b' } source.change.emit() """)``` and used a Hovertool to check the 'label' entry. In fact it changes in the plot. Nevertheless after checking source.data in the next cell it has no 'b' in source.data['label'] despite using source.change.emit(). – Andre S. Sep 27 '19 at 09:32
  • Is it even possible to change the source object within a jupyter-environment or do I have to set it up as a bokeh serve? – Andre S. Sep 27 '19 at 09:32
  • If you just want to change the JavaScript side, from Python, you can use `push_notebook`. If you want the Python side to update in response to changes in the JavaScript side, that requires a Bokeh server app. The Bokeh server *is the thing* that performs the work to keep both sides synchronized. – bigreddot Sep 27 '19 at 14:00
0

With the help of this guide on how to embed a bokeh server in the notebook I figured out the following minimal example for my purpose:

from random import random
import pandas as pd
import numpy as np
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, BoxSelectTool
from bokeh.models.widgets import TextInput
output_notebook()

def modify_doc(doc):
    # create a plot and style its properties
    TOOLS="pan,wheel_zoom,reset"
    p = figure(title = "My chart", tools=TOOLS)
    p.xaxis.axis_label = 'X'
    p.yaxis.axis_label = 'Y'
    hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "@label")])

    source = ColumnDataSource(
    data=dict(
            xvals=list(range(0, 10)),
            yvals=list(np.random.normal(0, 1, 10)),
            label = [i for i in "a"*10]
    ))
    p.scatter("xvals", "yvals",source=source, color="white")
    p.line("xvals", "yvals",source=source)
    p.add_tools(BoxSelectTool(dimensions="width"))
    p.add_tools(hovertool)

    # create a callback that will add a number in a random location
    def callback():
            inds = source.selected.indices
            for i in inds:
                    source.data['label'][i] = label_input.value.strip()
            print(source.data)
            new_data =  pd.DataFrame(source.data)
            new_data.to_csv("new_data.csv", index=False)

    # TextInput to specify the label
    label_input = TextInput(title="Label")
    # add a button widget and configure with the call back
    button = Button(label="Label Data")
    button.on_click(callback)

    # put the button and plot in a layout and add to the document
    doc.add_root(column(button,label_input, p))

show(modify_doc, notebook_url="http://localhost:8888")

That generates the following UI: enter image description here

BTW: Due to the non-existing box_select tool for the line glyph I use a workaround by combining it with invisible scatter points.

So far so good, is there a more elegant way to access the data.source/new_data df in the notebook outside modify_doc() than exporting it within the callback?

Community
  • 1
  • 1
Andre S.
  • 478
  • 4
  • 13