0
votes

using Bokeh, I am trying to update the .selected dictionary of a ColumnDataSource programmatically, via the callback of a Slider, but cannot manage to get the selection reflected in the plot.

In the following snippet, the idea is that I want to be able to make a y-axis selection both via the ybox_select tool and/or by adjusting the sliders that control the position of a pair of min/max lines (NOTE: for brevity, in this example I only included the 'max' slider and line). If possible, I want to achieve this without using CustomJS callbacks.

I got as far as adjusting the horizontal line and the slider value (and of course the selection, which happens implicitly) when I operate the ybox_select tool (which triggers the selection_change function). Instead, when I operate the slider (triggering the slider_selection function), I manage to control the horizontal line but, apparently, not the source selection. In other words, the modification of source.data that occurs in slider_selection is reflected in the plot (i.e. the modified position of the horizontal line) but the modification of source.selected is NOT reflected in the plot (nor in a DataTable, as I verified separately).

Following the suggestion in this thread (where I've asked a shorter version of this question but didn't get any answers so far), I've worked on a copy of source.selected and then copied back to .selected (same for .data), but this didn't have any effects.

I must be missing something rather fundamental, but cannot figure out what. Any idea? Please avoid suggestions based on CustomJS, unless you're sure that there is no pure-Python alternative.

Thanks a lot for any feedback!

(Note: run this code as a script with bokeh serve --show script.py)

from bokeh.io import curdoc
from bokeh.models import BoxSelectTool, Slider
from bokeh.plotting import figure, ColumnDataSource
from bokeh.sampledata.glucose import data
from bokeh.layouts import column
import numpy as np


#===============================================================================
# Data and source
y = data.ix['2010-10-06']['glucose']
x = np.arange(len(y))
maxval=[max(y)]*len(x)
source = ColumnDataSource(dict(x=x, y=y, maxval=maxval))

#===============================================================================
# Basic plot setup
tools = 'wheel_zoom,ybox_select,reset'
p = figure(plot_width=800, plot_height=400, tools=tools, title='Min/max selection')

# Plot data
cr = p.circle('x', 'y', color="blue", source = source,
              selection_color="blue", nonselection_color="gray", 
              size=6, alpha=0.8)

# Plot max horizontal line
p.line('x', 'maxval', line_color='blue', line_width=0.5, source=source,
       nonselection_alpha=1.0, nonselection_color='blue')

#===============================================================================
# Callbacks
def selection_change(attrname, old, new):
    ixs = new['1d']['indices']
    if ixs:
        arr = np.asarray(source.data['y'])[ixs]
        max_slider.value = np.max(arr)
        source.data['maxval'] = [np.max(arr)]*len(source.data['x'])

def slider_selection(attrname, old, new):
    selected = source.selected.copy()
    data = source.data.copy()
    data['maxval'] = [max_slider.value]*len(data['x'])
    yy = np.asarray(data['y'])
    maxi = np.asarray(data['maxval'])
    # Below is the new selection I would to visualize
    selected['1d']['indices'] = np.where(yy <= maxi)[0].tolist() 
    # Updated data is reflected in the plot (horizontal line at 'maxval' moves)
    source.data = data.copy()  
    # Updated selection is NOT reflected in the plot 
    # (nor in a DataTable, as tested separately)
    source.selected = selected.copy() 

#===============================================================================
# Slider
max_slider = Slider(start=min(y), end=max(y), 
                    value=max(y), step=0.1, title="Maximum")

#===============================================================================
# Trigger callbacks
source.on_change('selected', selection_change)
max_slider.on_change('value', slider_selection)

#===============================================================================
# Layout
plot_layout = column(p, max_slider)

curdoc().add_root(plot_layout)
curdoc().title = "Demo"
1

1 Answers

0
votes

Adding the following line to slider_selection seems to do what you want:

source.trigger("selected", old, selected)

the new function definition:

def slider_selection(attrname, old, new):
    selected = source.selected.copy()
    data = source.data.copy()
    data['maxval'] = [max_slider.value]*len(data['x'])
    yy = np.asarray(data['y'])
    maxi = np.asarray(data['maxval'])
    # Below is the new selection I would to visualize
    selected['1d']['indices'] = np.where(yy <= maxi)[0].tolist()
    # Updated data is reflected in the plot (horizontal line at 'maxval' moves)
    source.data = data.copy()
    # Updated selection is NOT reflected in the plot
    # (nor in a DataTable, as tested separately)
    source.selected = selected.copy()
    source.trigger("selected", old, selected)

(Though it's a bit late, I found your question trying to find a similar answer, I figured this might be useful to others).