2

I am struggling to understand how to incorporate js_on_change for self-embedded bokeh stacked bar graph with dropdown selection. In a nutshell, whenever we select a value in the dropdown menu, it should map itself into a list of columns of the main dataframe that can be used then for stacked bar plotting.

I think I am lacking knowledge of Javascript and how to play around with it. Stackers, need your help. Below you will find the complete code that should run on your end.

I borrowed a little from this thread bokeh - plot a different column using customJS

import pandas as pd
import numpy as np
import datetime

from bokeh.io import show
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure, ColumnDataSource, show
from bokeh.models.widgets import Select
from bokeh.models import Label, Title, NumeralTickFormatter

df = pd.DataFrame.from_dict(
    {
 'Apples_green': {'2018': 100, '2019': 150, '2020': 200},
 'Apples_red': {'2018': 200, '2019': 75, '2020': 25},
 'Oranges_green': {'2018': 25, '2019': 60, '2020': 70},
 'Oranges_red': {'2018': 100, '2019': 80, '2020': 10}
    }
 )

#List of columns for apples
initial_col = [i for i in df.columns.tolist() if i.startswith('Apples')]
selection_list = ["Apples", "Oranges"]

d_map = {
  'Apples':['Apples_green', 'Apples_red'],
  'Oranges':['Oranges_green', 'Oranges_red']
}

source = ColumnDataSource(df)

p = figure(plot_width=350, plot_height = 300,
        x_range=df.index.drop_duplicates().tolist())
p.vbar_stack(initial_col, x='index',
 width=0.9, color=['green', 'red'], source=source)
p.yaxis.formatter = NumeralTickFormatter(format='(0.00)')


select = Select(title="Fruit:", value=selection_list[0], options=selection_list)

select.js_on_change('value', CustomJS(args=dict(source=source, select=select, d_map = d_map), code="""
 // print the selectd value of the select widget - 
 // this is printed in the browser console.
 // cb_obj is the callback object, in this case the select 
 // widget. cb_obj.value is the selected value.
 console.log(' changed selected option', cb_obj.value);

  // create a new variable for the data of the column data source
  // this is linked to the plot
  var data = source.data;

  // allocate the selected column to the field for the y values

  data['{}'] = data[d_map[cb_obj.value]];

  // register the change - this is required to process the change in 
  // the y values
  source.change.emit();
""".format(d_map[selection_list[0]])))

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

Eventually, it plots the figure but javascript part is not working.

See screenshot here

tmrkv
  • 23
  • 4

1 Answers1

1

The immediate problem with your code is the way you format the JS code string passed to CustomJS. It ends up being a broken JS code - you could see the error by opening your browser's JS console and changing the value of the Select widget.

As for your actual task - it's not that easy to do because vbar_stack is not a real glyph function. Instead, it's just a helper function that calls the vbar glyph function with the necessary parameters. Your call to p.vbar_stack(...) effectively becomes

p.vbar(bottom=stack(), top=stack('Apples_green'),
       x='index', width=0.9, color='green', source=source)
p.vbar(bottom=stack('Apples_green'), top=stack('Apples_green', 'Apples_red'),
       x='index', width=0.9, color='red', source=source)

That means that in order to change the columns which the expressions used by the VBar glyphs use, you will have to basically convert that Bokeh Python code into BokehJS JavaScript code.

With that being said, here's a working version of your code that should work for any number of colors, not just two:

import pandas as pd

from bokeh.layouts import row, column
from bokeh.models import CustomJS
from bokeh.models import NumeralTickFormatter
from bokeh.models.widgets import Select
from bokeh.plotting import figure, ColumnDataSource, show
from bokeh.transform import stack

df = pd.DataFrame.from_dict(
    {
        'Apples_green': {'2018': 100, '2019': 150, '2020': 200},
        'Apples_red': {'2018': 200, '2019': 75, '2020': 25},
        'Oranges_green': {'2018': 25, '2019': 60, '2020': 70},
        'Oranges_red': {'2018': 100, '2019': 80, '2020': 10}
    }
)

d_map = {
    'Apples': ['Apples_green', 'Apples_red'],
    'Oranges': ['Oranges_green', 'Oranges_red']
}

selection_list = list(d_map.keys())  # In modern Python, dicts are ordered by default.
initial_value = selection_list[0]
initial_col = [i for i in df.columns.tolist() if i.startswith(initial_value)]

source = ColumnDataSource(df)

p = figure(plot_width=350, plot_height=300,
           x_range=df.index.drop_duplicates().tolist())
renderers = []
col_acc = []
for col in d_map[initial_value]:
    color = col[len(initial_value) + 1:]
    r = p.vbar(bottom=stack(*col_acc), top=stack(col, *col_acc),
               x='index', width=0.9, color=color, source=source)
    col_acc.append(col)
    renderers.append(r)
p.yaxis.formatter = NumeralTickFormatter(format='(0.00)')

select = Select(title="Fruit:", value=initial_value, options=selection_list)

select.js_on_change('value', CustomJS(args=dict(d_map=d_map, renderers=renderers),
                                      code="""
    const Stack = Bokeh.Models('Stack');
    const col_acc = [];
    d_map[cb_obj.value].forEach((col, idx) => {
        const {glyph} = renderers[idx];
        glyph.bottom = {expr: new Stack({fields: col_acc})};
        col_acc.push(col);
        glyph.top = {expr: new Stack({fields: col_acc})};
    });
"""))

col = column(select)
layout = row(col, p)
show(layout)
Eugene Pakhomov
  • 9,309
  • 3
  • 27
  • 53
  • Thank you, Eugene. Worked like a charm. Additional credits go for making it ready to scale up in terms of inputs! – tmrkv May 05 '20 at 17:25