Problems to change interactively (widgets + callbacks) the number of lines in multi lines plots

I was following the recommendations of @p-himik and @carolyn to deal with multiple lines and callbacks. The included code shows my example. My problem is how to change interactively the number of lines of graph? In line with the approach of @p-himik the problem is how to change the number of elements in the line_renderers variable? In other words how to build new glyphs through one or more callbacks?

import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS, MultiChoice, HoverTool
from bokeh.layouts import row, column, layout
from bokeh.plotting import figure, show
from bokeh.palettes import Category20_20

df = pd.DataFrame(data={'ILLUMINANT': {0: 'l33t', 1: 'l33t', 2: 'l33t', 3: 'l33t', 4: 'l33t', 5: 'l33t', 6: 'l33t', 7: 'l33t', 8: 'l33t', 9: 'l33t', 10: 'l33t', 11: 'l33t', 12: 'noob', 13: 'noob', 14: 'noob', 15: 'noob', 16: 'noob', 17: 'noob', 18: 'noob', 19: 'noob', 20: 'noob', 21: 'noob', 22: 'noob', 23: 'noob'}, 'WAVELENGTH': {0: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 2, 7: 2, 8: 2, 9: 3, 10: 3, 11: 3, 12: 0, 13: 0, 14: 0, 15: 1, 16: 1, 17: 1, 18: 2, 19: 2, 20: 2, 21: 3, 22: 3, 23: 3}, 'Label': {0: 'year01', 1: 'year02', 2: 'year03', 3: 'year01', 4: 'year02', 5: 'year03', 6: 'year01', 7: 'year02', 8: 'year03', 9: 'year01', 10: 'year02', 11: 'year03', 12: 'year01', 13: 'year02', 14: 'year03', 15: 'year01', 16: 'year02', 17: 'year03', 18: 'year01', 19: 'year02', 20: 'year03', 21: 'year01', 22: 'year02', 23: 'year03'}, 'Data': {0: 0.0010402764, 1: 0.0011201306, 2: 0.0011656343, 3: 8.00911e-05, 4: 0.0001257282, 5: 0.00016172540000000002, 6: 4.56483e-05, 7: 8.16531e-05, 8: 0.0001140501, 9: 3.60118e-05, 10: 6.841e-05, 11: 0.0001036948, 12: 0.0012474055, 13: 0.0013409409, 14: 0.001397252, 15: 9.38607e-05, 16: 0.0001503632, 17: 0.00020415279999999998, 18: 5.65171e-05, 19: 0.0001103194, 20: 0.00014596190000000002, 21: 5.38097e-05, 22: 8.94597e-05, 23: 0.00012082139999999999}})

df = df.astype('object')
 
source_inicial = ColumnDataSource(data=df)
 
ILLUMINANTSS = df.ILLUMINANT.unique().tolist()
 
ILLUMINANT_select = MultiChoice(value=list(ILLUMINANTSS[:1]), options=ILLUMINANTSS, title="Illuminants")

df_filtered = df[df["ILLUMINANT"].isin(ILLUMINANT_select.value)]
 
CURVASS = df_filtered.Label.unique().tolist()
                           
line_fig = figure(x_range=(0, 3), y_range=(0, .002), sizing_mode="stretch_both")

hover_lines = HoverTool(
    tooltips=[
        ('Illuminant', '@ILLUMINANT'),
        ('Curva', '@Label'),
        ('SRF', '@Data'),
        ('Wavelength', '@WAVELENGTH')
    ]
)
line_fig.add_tools(hover_lines)

line_renderers = []
line_renderer_source_subsets = []
 
for cause, color in zip(CURVASS, Category20_20):
    r = line_fig.line(x='WAVELENGTH'
                      , y='Data'
                      , color=color
                      , line_width=3
                      , source=ColumnDataSource(data=df_filtered[df_filtered.Label == cause].reset_index(drop=True))
                      , legend_label=str(cause)
                    )
    # print(cause, color)
    line_renderers.append(r)
    # for each line, maintain a subset that is filtered by CURVAS but not by ILLUMINANT; this is the "master" source
    # for that line, which will then be filtered by ILLUMINANT in the ILLUMINANT callback.
    line_renderer_source_subsets.append(ColumnDataSource(data=df_filtered[df_filtered.Label == cause].reset_index(drop=True)))

fields_to_update = ['index'] + list(df_filtered.columns.values)

ILLUMINANT_select_callback = CustomJS(args=dict(source_inicial=source_inicial,
                                                        line_renderers=line_renderers,
                                                        line_renderer_source_subsets=line_renderer_source_subsets,
                                                        ILLUMINANT_select=ILLUMINANT_select,
                                                        fields_to_update=fields_to_update), code="""
    for (var m = 0; m < line_renderers.length; m++) {
        line_renderers[m].data_source.clear();
        for (var n = 0; n < line_renderer_source_subsets[m].data.index.length; n++) {
            for (var o = 0; o < source_inicial.data.index.length; o++) {
                if (source_inicial.data.ILLUMINANT[o] == ILLUMINANT_select.value & source_inicial.data.Label[o] == line_renderer_source_subsets[m].data.Label[n] & source_inicial.data.WAVELENGTH[o] == line_renderer_source_subsets[m].data.WAVELENGTH[n]) {
                    for (var f in fields_to_update) {
                        var field = fields_to_update[f];
                        line_renderers[m].data_source.data[field].push(source_inicial.data[field][o]);
                    }
                }
            }
        }
        line_renderers[m].data_source.change.emit();
    }
""")

ILLUMINANT_select.js_on_change('value', ILLUMINANT_select_callback )

top_area = row(ILLUMINANT_select)
show(layout(column([top_area, line_fig]), sizing_mode="stretch_both"))

I have tried using CDSview but it does not work with multiple lines. In my low level of understanding, also it is not possible to filter the source before the loop using a callback, is that correct?
Thank you for your suggestion and comments.

CDSView does work with multiline. Basically a copy-paste of my answer on the other thread.

The idea is to make an index filter that filters nothing initially (i.e. when all checkboxes are checked), then to update the indices stored in that filter based on the active checkbox groups. It’s basically the exact same paradigm:

from bokeh.layouts import row
from bokeh.models import CheckboxGroup, ColumnDataSource, CustomJS, CDSView, IndexFilter
from bokeh.plotting import curdoc, figure, show

source = ColumnDataSource(data=dict(
    ls = ['L0','L1','L2'],
    xs=[[1,2,3], [1,3,4], [2, 3, 4]],
    ys=[[1,3,2], [5,4,3], [9, 8, 9]],
    alpha=[1, 1, 1]
))

p = figure(y_range=(0, 10))

#make a view with a single index filter
filt = IndexFilter() #initialize filter
view = CDSView(source=source,filters=[filt])

p.multi_line("xs", "ys", line_alpha="alpha", line_width=3, source=source,view=view)

cb = CheckboxGroup(labels=["L0", "L1", "L2"], active=[0, 1, 2])


update = CustomJS(args=dict(cb=cb,source=source,filt=filt)
                  ,code='''
                 
                 //get the labels corresponding to the active checkbox groups
                 //look up how JS map works
                 var active_lbls = cb.active.map(x=>cb.labels[x])
                 //then populate new array of filter indices
                 var new_filt = []
                 for (var i=0;i<source.data['ls'].length;i++){
                         if (active_lbls.includes(source.data['ls'][i])){
                                 new_filt.push(i)
                                 }                        
                         }
                 //update the index filter
                 filt.indices = new_filt
                 source.change.emit()
                 '''
                  )


cb.js_on_change('active', update)

show(row([p, cb]))
1 Like

Hello Gaelen @gmerritt123 I have worked on your example to include MultiChoice. Below is the current code.
Now the problem is that at the beginning the widget show one option, but the graph show all three curves. When I pre-filtered the source, it shows one curve and one option but it is not possible to include more curves. Any idea how to start with one plot, one option and then to include more curves? Thank you. Rodrigo

import pandas as pd
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, CustomJS, MultiChoice
from bokeh.plotting import curdoc, figure, show

df = pd.DataFrame(data={'ls': ['L0', 'L1', 'L2'], 'xs': [[1, 2, 3], [1, 3, 4], [2, 3, 4]], 'ys': [[1, 3, 2], [5, 4, 3], [9, 8, 9]], 'alpha': [1, 1, 1]})

ILLUMINANTSS = df.ls.unique().tolist()

cb = MultiChoice(value=list(ILLUMINANTSS[:1]), options=ILLUMINANTSS, title="Illuminants")

## this is the ideally source that show just one option. Do not work properly
df_filtered = df[df["ls"].isin(cb.value)]
source_filtered = ColumnDataSource(data=df_filtered)
##

source = ColumnDataSource(data=df)

p = figure(y_range=(0, 10))

p.multi_line("xs", "ys", line_alpha="alpha", line_width=3, source=source)

update = CustomJS(args=dict(cb=cb,source=source)
        ,code='''
        
    //get the labels corresponding to the active checkbox groups
    //look up how JS map works
    var active_lbls = cb.value
    //then populate new array of alphas for the source based on this
    var new_alphas = []
    for (var i=0;i<source.data['ls'].length;i++){
            if (active_lbls.includes(source.data['ls'][i])){
                    new_alphas.push(1)
                    }
            else {new_alphas.push(0)}                         
            }
    //assign the new alphas
    source.data['alpha'] = new_alphas
    source.change.emit()
    '''
    )

cb.js_on_change('value', update)

show(row([p, cb]))

Just initialize your alpha array in the dataframe to align with the initial state of the multichoice.

So like,

#initial state has first line with alpha =1, the other lines get an initial alpha of 0
df = pd.DataFrame(data = {.... 'alpha':[1,0,0]}

Hello Gaelen,

It worked very nice. Thank you.

I am working in more ambitious project on this topic, multiple lines selected from six different widgets. I am curious if you have time to offer some consulting on this specific topic. What do you think?

Kind regards,

Rodrigo