Bokeh wedge piechart datasource when pressed on bar from barchart (javascript)

Hi,

I’m quite new to bokeh and im trying to change de datasource of my wedge by giving it a element from a list based on the index var selected_index = cb_data.source.selected.indices[0];. But when I try pie.data_source = source_list[selected_index]; it doest work. I would like to know if this is possible or not. In the end I would like to run it with bokeh serve.

—Code— version 3.0.2

from bokeh.models import ColumnDataSource
from bokeh.transform import cumsum
from bokeh.palettes import Category10
from bokeh.plotting import figure, show
from bokeh.layouts import row
from random import randint
import pandas as pd
from math import pi

def maybe():
        source_list = []
        for key, value_ in z.items():
                data = pd.Series(value_).reset_index(name='value').rename(columns={'index': 'country'})
                data['angle'] = data['value']/data['value'].sum() * 2*pi
                data['color'] = Category10[len(value_)]
                source_list.append(ColumnDataSource(data))
        return source_list        
source_list = maybe()               

        
source_bars = ColumnDataSource({'x': [1, 2, 3, 4, 5], 'y': [3,3,3,3,3]})
p = figure(height = 500, width = 800, tools = 'tap')
bars = p.vbar(x = 'x', top = 'y', source = source_bars, bottom = 0, width = 0.2)
p2 = figure(height = 500, width = 800)

pie = p2.wedge(x=0, y=1, radius=0.4,
        start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
        line_color="white", fill_color='color', source=source_list[0])
# pie.visible = False

p2.axis.axis_label = None
p2.axis.visible = False
p2.grid.grid_line_color = None

code = '''if (cb_data.source.selected.indices.length > 0){
            pie.visible = true;
            var selected_index = cb_data.source.selected.indices[0];
            pie.data_source = source_list[selected_index];
            pie.data_source.change.emit(); 
          }'''
plots = row(p, p2)
p.select(TapTool).callback = CustomJS(args = {'pie': pie, 'source_list': source_list}, code = code)
show(plots)

Thanks in advanced.

This isn’t quite a fully contained MRE. In your maybe function, z is not defined. Looks like a dictionary because you iterate through it’s key+values. Can you make a “dummy” version of it for this example so I have something to run?

Sorry I forgot the z its a dictionary
code

z = {0: {'A': 23.0, 'C': 1.0, 'G': 4.0, 'T': 21.0},
 1: {'A': 23.0, 'C': 2.0, 'G': 6.0, 'T': 18.0},
 2: {'A': 18.0, 'C': 4.0, 'G': 9.0, 'T': 18.0},
 3: {'A': 0.0, 'C': 8.0, 'G': 2.0, 'T': 39.0},
 4: {'A': 2.0, 'C': 1.0, 'G': 40.0, 'T': 6.0},
 5: {'A': 1.0, 'C': 8.0, 'G': 0.0, 'T': 40.0},
 6: {'A': 4.0, 'C': 0.0, 'G': 41.0, 'T': 4.0},
 7: {'A': 43.0, 'C': 3.0, 'G': 3.0, 'T': 0.0},
 8: {'A': 6.0, 'C': 10.0, 'G': 9.0, 'T': 24.0},
 9: {'A': 9.0, 'C': 17.0, 'G': 10.0, 'T': 13.0},
 10: {'A': 9.0, 'C': 15.0, 'G': 5.0, 'T': 20.0},
 11: {'A': 15.0, 'C': 7.0, 'G': 17.0, 'T': 10.0},
 12: {'A': 10.0, 'C': 8.0, 'G': 21.0, 'T': 10.0},
 13: {'A': 16.0, 'C': 11.0, 'G': 7.0, 'T': 15.0},
 14: {'A': 6.0, 'C': 3.0, 'G': 7.0, 'T': 33.0},
 15: {'A': 7.0, 'C': 39.0, 'G': 0.0, 'T': 3.0},
 16: {'A': 35.0, 'C': 1.0, 'G': 10.0, 'T': 3.0},
 17: {'A': 10.0, 'C': 32.0, 'G': 0.0, 'T': 7.0},
 18: {'A': 33.0, 'C': 4.0, 'G': 6.0, 'T': 6.0},
 19: {'A': 12.0, 'C': 9.0, 'G': 7.0, 'T': 21.0},
 20: {'A': 18.0, 'C': 1.0, 'G': 0.0, 'T': 30.0},
 21: {'A': 14.0, 'C': 7.0, 'G': 4.0, 'T': 24.0}}

So the reason why this isn’t working is that you are using the cumsum transform (bokeh actually treats this as a specific type of expression) and there seems to be some missing plumbing/potentially unintended behaviour with it. If you wanna get into the weeds read on, but there’s a super easy workaround at the end anyway.

The Weeds

If you look at the start_angle attribute on the python side:
image

You’ll see a CumSum “expression” model.

If you look at that thing on the JS side, i.e. console.log(pie.glyph.start_angle.expr) in your CustomJS:

I’ve noticed/picked out two things:

  1. That in the [[Prototype]], there is a _v_compute(f) function that (upon browsing the source code → bokeh/cumsum.ts at branch-3.1 · bokeh/bokeh · GitHub), seems to take the underlying datasource as an argument.

  2. the _result returns a Map that seems to map the datasource to the “computed” result i.e. the cumulative sum of angle.

My initial assumption was that we simply needed to call v_compute once the data source gets updated:

//....
pie.data_source = source_list[selected_index];
pie.data_source.change.emit()
pie.glyph.start_angle['expr']._v_compute(pie.data_source)
pie.glyph.end_angle['expr']._v_compute(pie.data_source)

However, this doesn’t work, because it seems like the Map in _result still maps to the original source not the updated one:

pie.data_source = source_list[selected_index];
pie.data_source.change.emit()
//logging the mapped result before calling _v_compute
console.log(pie.glyph.start_angle['expr']._result)
pie.glyph.start_angle['expr']._v_compute(pie.data_source)
pie.glyph.end_angle['expr']._v_compute(pie.data_source)
//calling it after...
console.log(pie.glyph.start_angle['expr']._result)

^^Notice the two id’s are identical but they should be different, or at least you want them to be.

I’m not certain if this is some unforeseen consequence of dev-design or is actually intended (cc @Bryan ?)

The Easy Workaround

So after beating that to death in terms of gaining an understanding, the “easy” workaround then should be to not pass a list of CDS’s to the CustomJS, but instead only make one, and update that CDS by passing a dictionary with which you can update its data property:

def maybe():
        source_list = []
        for key, value_ in z.items():
                data = pd.Series(value_).reset_index(name='value').rename(columns={'index': 'country'})
                data['angle'] = data['value']/data['value'].sum() * 2*pi
                data['color'] = Category10[len(value_)]
                # source_list.append(ColumnDataSource(data))
                source_list.append({x:data[x].tolist() for x in data.columns})
        return source_list        
source_list = maybe()   

and then in your callback, replace

pie.data_source = source_list[selected_index];

with

pie.data_source.data = source_list[selected_index];

1 Like

Hi Gaelen,

Thanks for you’re detailed reply! If I’m honest I understand somethings but my javascript knowledge isn’t that great. But I really appreciate the time for explaining!

Kind regard,
Jesse