CustomJS widget to control all glyphs on plot with an UNKNOWN number of sources

The Goal:
Create a widget that uses a CustomJS callback to control a number of glyphs at the same time … BUT the number of glyphs (and how many underlying ColumnDataSources there are) is unspecified, because it depends on input data.

The Problem:
While there are plenty of examples on this forum of a single widget controlling multiple glyphs, I’ve never seen it implemented in such a way as to accommodate an unspecified number of sources. I will not know how many glyphs will be required beforehand, as they are created as part of a for-loop that iterates through user data.

Suppose I have some dictionaries with data that I want to turn into ColumnDataSources - originating from a for loop such that:

sources = []
original_dict: = {'x_value': 1,
                  'y_value': 2}
q = 0

for q < int('user_input_value'):
    new_dict = original_dict[key] += 1
    new_source = ColumnDataSource(data=new_dict)
    sources.append(new_source)
    q += 1

Then we use the list of sources and make glyphs for each:

p = figure(width=400, height=400)
for source in sources:
    p.circle(x='x_value', y='y_value', source=source)

Now suppose I want to throw down a slider:

callback = CustomJS(args=dict(source=source), code="""
    const data = source.data;
    const f = cb_obj.value
    const x = data['x']
    const y = data['y']
    for (let i = 0; i < x.length; i++) {
        y[i] = Math.pow(x[i], f)
    }
    source.change.emit();
""")

slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)

But the source argument in the callback is the rub - I know I could specifiy sources in sequence - source1=source1, source2=source2, etc., but that presupposes a known number of sources.

I thought to try a list comprehension of sorts:

callback = CustomJS(args=dict(source = [source for source in sources]), 
code="""
JS code goes here 
"""

Which proved ineffective.

Is there a way to do this?

Ha, you’re pretty much there.

Basically your first callback is it:

Now suppose I want to throw down a slider:

callback = CustomJS(args=dict(source=source), code="""
    const data = source.data;
    const f = cb_obj.value
    const x = data['x']
    const y = data['y']
    for (let i = 0; i < x.length; i++) {
        y[i] = Math.pow(x[i], f)
    }
    source.change.emit();
""")

slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)

You just need to pass all sources to this as an arg, which you can easily access now since you populated a list of them already (i.e. “sources”), then just nest another JS loop iterating through all of them, performing the same update each time. Something like:

callback = CustomJS(args=dict(sources=sources), code="""
    for (var ii=0; ii<sources.length;ii++){
        var source = sources[ii]
        var data = source.data;
        var f = cb_obj.value
        var x = data['x']
        var y = data['y']
        for (let i = 0; i < x.length; i++) {
            y[i] = Math.pow(x[i], f)
        }
        source.change.emit();
        }
""")

The only other change I made was your const variables become vars because you’re modifying them each time in the loop now.

EDIT: also notice how i’m iterating through ii on the outer loop because your inner loop iterates through i… (danger zone :joy:)

2 Likes

Oh, thanks for the corrections!! My real use case uses much more complicated data - I apologize for my errors in the hastily-thrown-together example.

Hi again - your solution below worked great for a simple slider, but can this be adapted for more complicated - even custom - widgets?

Suppose that my js_on_change callback took a ‘range’ instead of a ‘value’, such that:

    ion_range_slider = IonRangeSlider(start=x_min, end=x_max, step=1, range=(x_min, x_max),
                                      title='Ion Range Slider - Range')
    ion_range_slider.js_on_change('range', callback_ion)

(yes, yes, I am attempting to adapt the custom widget found here: Adding a custom widget — Bokeh 2.4.2 Documentation)

I tried to adjust var f, as well as what y[i] might be equal to, in order to accommodate this - but the html console has always shown that f and y[i] are undefined.

Just glancing at that custom widget example, it looks like cb_obj doesn’t have a “value” property anymore but instead it has a “range” property.

So in your callback using the Ion Range Slider, to access the “left” bound of the slider and the "right bound, you’d do something like this:

var fmin = cb_obj.range[0]
var fmax = cb_obj.range[1]

Instead of

var f = cb_obj.value

FYI, my go to for figuring this stuff out is to console.log the living daylights out of the JS code and look in my browser console. So put console.log(cb_obj) somewhere in your js code, and you’d be able to inspect that object and see what’s available to you etc.

1 Like