Bar chart with select widget

Hello!

I’m trying to create a bar chart that is updated by option selected in select widget using customJS.

But it is not updating the plot I don’t know why.

This is my code:

import numpy as np

notif = ['Notificacao1', 'Notificacao2', 'Notificacao3', 'Notificacao4',

'Notificacao5', 'Notificacao6', 'Notificacao7', 'Notificacao8', 'Notificacao9',

'Notificacao10', 'Notificacao11', 'Notificacao12', 'Notificacao13', 

'Notificacao14', 'Notificacao15', 'Notificacao16', 'Notificacao17',

'Notificacao18', 'Notificacao19', 'Notificacao20', 'Notificacao21',

'Notificacao22', 'Notificacao23', 'Notificacao24', 'Notificacao25',

'Notificacao26', 'Notificacao27', 'Notificacao28', 'Notificacao29',

'Notificacao30']

notif = np.array(notif)

group = [1] * 10 + [2] * 10 + [3] * 10

group = np.array(group)

import pandas as pd

df = pd.DataFrame(dict(notif = notif,group = group))

from bokeh.io import show, output_file

from bokeh.plotting import figure

from bokeh.models import Select,ColumnDataSource, CustomJS

from bokeh.layouts import column

source = ColumnDataSource(data = df)

p = figure(y_range = source.data['notif'][source.data['group'] == 1].tolist())

p.hbar(y = 'notif', source = source, height = .9, right = .1)

select = Select(title = 'Grupo', value = '1', options = ['1','2','3'])

callback = CustomJS(args = dict(source = source, select = select), 

                    code = """ var data = source.data;

                               var G = int(select.value);

                               group = data['group']

                               data['notif'] = data['notif'][group == G]

                               data['group'] = data['group'][group == G]                        

                               

                    source.change.emit()""")

select.js_on_change('value', callback)

layout = column(select, p)

show(layout)

Hi lunna.borges!

I think conceptually what you want is actually a CustomJSFilter. I played with your code a little bit to work up an example. The tricky thing ended up being your categorical y-axis, so in this example we have the CustomJSFilter to actually do the work of filtering the dataset, and a CustomJS callback to reset the y-axis values to match your new view. (I would love to see this done a shorter way, but this works!)

import numpy as np
import pandas as pd
from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models import Select, ColumnDataSource, CustomJS, CustomJSFilter, CDSView
from bokeh.layouts import column

notif = ['Notificacao1', 'Notificacao2', 'Notificacao3', 'Notificacao4', 'Notificacao5', 'Notificacao6', 'Notificacao7',
         'Notificacao8', 'Notificacao9', 'Notificacao10', 'Notificacao11', 'Notificacao12', 'Notificacao13',
         'Notificacao14', 'Notificacao15', 'Notificacao16', 'Notificacao17', 'Notificacao18', 'Notificacao19',
         'Notificacao20', 'Notificacao21','Notificacao22', 'Notificacao23', 'Notificacao24', 'Notificacao25',
         'Notificacao26', 'Notificacao27', 'Notificacao28', 'Notificacao29', 'Notificacao30']

notif = np.array(notif)

group = [1] * 10 + [2] * 10 + [3] * 10
group = np.array(group)

df = pd.DataFrame(data={'notif': notif, 'group': group})
source = ColumnDataSource(data=df)

select = Select(title='Grupo', value='1', options=['1', '2', '3'])

p = figure(y_range=df['notif'].loc[df['group'] == int(select.value)].tolist())

custom_filter = CustomJSFilter(args=dict(source=source, select=select), code='''
var indices = [];
// iterate through rows of data source and see if each satisfies some constraint
for (var i = 0; i < source.get_length(); i++){
    if (source.data['group'][i] == select.value){
        indices.push(true);
    } else {
        indices.push(false);
    }
}
return indices;
''')

filtered_view = CDSView(source=source, filters=[custom_filter])
p = figure(y_range=df['notif'].loc[df['group'] == int(select.value)].tolist())
bars = p.hbar(y='notif', source=source, height=.9, right=.1, view=filtered_view)

callback = CustomJS(args=dict(source=source, select=select, p=p), code="""
    new_y_range = [];
    for (var i = 0; i < source.get_length(); i++){
      if (source.data['group'][i] == select.value)
          new_y_range.push(source.data['notif'][i]);
    }
    p.y_range.factors = new_y_range;
    source.change.emit()
    """)
select.js_on_change('value', callback)

layout = column(select, p)
show(layout)

Dear carolyn,

thank you so much to answer my question.

I’m excited to run the code and see the final result of it but I’m having problems to import CustomJSFilter from bokeh.models module.

When I try to do it, I get the following error:

ImportError                               Traceback (most recent call last)
Untitled-1 in ()
----> 1 from bokeh.models import CustomJSFilter

ImportError: cannot import name 'CustomJSFilter'

I believe it is because of a difference in bokeh package version but the only version of bokeh that I can install is 0.12.5, I don’t know why :slightly_frowning_face:

I’m using vscode, python 3.5.2 (64-bit) and satisfying the conditions bellow:

Jinja2 >=2.7
numpy >=1.7.1
packaging >=16.8
pillow >=4.0
python-dateutil >=2.1
PyYAML >=3.10
six >=1.5.2
tornado >=4.3

Could you help me understand what is going on?

Hi @lunna.borges unfortunately 0.12.5 is way too old, CustomJSFilter was added some time after that release. So that will not be an option.

There’s two main issues with the code you have already. The first is this:

JavaScript does not support any Pandas-like fancy indexing. You will need to write an explicit loop that loops over the entire data set and pulls out the things you care about. And then the second issue is that you will need to pass two CDS the CustomJS code: a “full” CDS that always has all the data, and a “glyph” CDS that has the subset you want to drive the glyph. If you just send one CDS and then overwrite it as you are now, as soon as you make one update, you are throwing away information you can never get back.

Here is an example that may be a useful reference:

https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-selections

Hello Carolyn. I am studying your code, but there is no graph to see. Do you have any updated version? Thank you. Rodrigo

Hi @CONSUMERTEC,

Try this:

import numpy as np
import pandas as pd
from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models import Select, ColumnDataSource, CustomJS, CustomJSFilter, CDSView
from bokeh.layouts import column

notif = ['Notificacao1', 'Notificacao2', 'Notificacao3', 'Notificacao4', 'Notificacao5', 'Notificacao6', 'Notificacao7',
         'Notificacao8', 'Notificacao9', 'Notificacao10', 'Notificacao11', 'Notificacao12', 'Notificacao13',
         'Notificacao14', 'Notificacao15', 'Notificacao16', 'Notificacao17', 'Notificacao18', 'Notificacao19',
         'Notificacao20', 'Notificacao21','Notificacao22', 'Notificacao23', 'Notificacao24', 'Notificacao25',
         'Notificacao26', 'Notificacao27', 'Notificacao28', 'Notificacao29', 'Notificacao30']

notif = np.array(notif)

group = [1] * 10 + [2] * 10 + [3] * 10
group = np.array(group)

df = pd.DataFrame(data={'notif': notif, 'group': group})
source = ColumnDataSource(data=df)

select = Select(title='Grupo', value='1', options=['1', '2', '3'])

p = figure(y_range=df['notif'].loc[df['group'] == int(select.value)].tolist())

custom_filter = CustomJSFilter(args=dict(cds=source, select=select), code='''
var indices = [];
// iterate through rows of data source and see if each satisfies some constraint
for (var i = 0; i < cds.get_length(); i++){
    if (cds.data['group'][i] == select.value){
        indices.push(true);
    } else {
        indices.push(false);
    }
}
return indices;
''')

filtered_view = CDSView(source=source, filters=[custom_filter])
p = figure(y_range=df['notif'].loc[df['group'] == int(select.value)].tolist())
bars = p.hbar(y='notif', source=source, height=.9, right=.1, view=filtered_view)

callback = CustomJS(args=dict(source=source, select=select, p=p), code="""
    var new_y_range = [];
    for (var i = 0; i < source.get_length(); i++){
      if (source.data['group'][i] == select.value)
          new_y_range.push(source.data['notif'][i]);
    }
    p.y_range.factors = new_y_range;
    source.change.emit()
    """)
select.js_on_change('value', callback)

layout = column(select, p)
show(layout)

Hello Carolyn,
It worked. Thank you for this example. Comments:
… the change from “source” to “cds” did the difference. What is the reason?
… you repeat the same line of code about “p= figure…” I removed the first one and also worked. What was the purpose to include it in your code twice?

Regards from Ecuador,
Rodrigo

CustomJSFilter already has source implied-- it will be the source of the CDSView filtered_view. The two variables having the same name was throwing an error (visible in the browser’s JS console). In fact, it was redundant for me to pass in cds at all; a better version would be

custom_filter = CustomJSFilter(args=dict(select=select), code='''
var indices = [];
// iterate through rows of data source and see if each satisfies some constraint
for (var i = 0; i < source.get_length(); i++){
    if (source.data['group'][i] == select.value){
        indices.push(true);
    } else {
        indices.push(false);
    }
}
return indices;
''')

And re the two figure declarations: no reason. An oversight. The first one does nothing, because it’s overwritten by the second one.

FYI in upcoming 2.3 attempting to pass source to a filter will raise an immediate runtime error.

1 Like

Hello @carolyn and @Bryan. Thank you for your response and observations. Let me comments some points concerning my experience with Bokeh which probably will be interesting to some members of the community:
… I started developing a web app to process lab data, some months ago, with Python-Flask. A month ago it was imperative to include graphs, so I started to study alternatives. My first approach was Dash (the open source version) based on internet references.
… we get the graph we were looking for. When I tried to embed the graphs in the app, though collision appears when the intention is not to graph some static file, but some pending on the user and projects. Finally I abandoned the tool with high level of frustration
… I started with Bokeh some three weeks ago. Firstly I decided with Bokeh Server in line with what I have tried with Dash. Finally it did not work for nearly the same previously reason. Server incompatibilities, more if your application is working in Pythonanywhere.
… one week ago I started with Bokeh JS and I finally reach similar results with no collisions neither server incompatibilities.

Thank you for the examples you provide in the environment and your time and patience with such a novice user. I will fully recommend this versatile tool.
Kind regards,
Rodrigo