Python / Bokeh / Javascript / js_on_change 用于堆叠栏和下拉列表选择

Python / Bokeh / Javascript / js_on_change for stacked bar and dropdown list selection

我正在努力了解如何将 js_on_change 用于带有下拉菜单 selection 的自嵌入散景堆叠条形图。 简而言之,每当我们 select 下拉菜单中的一个值时,它应该将自己映射到主数据框的列列表中,然后可以将其用于堆叠条形图。

我想我对 Javascript 以及如何使用它缺乏了解。堆垛机,需要你的帮助。您将在下面找到应该 运行 的完整代码。

我从这个帖子中借鉴了一点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)

最终,它绘制了图形,但 javascript 部分不起作用。

See screenshot here

您的代码的直接问题是您格式化传递给 CustomJS 的 JS 代码字符串的方式。它最终成为损坏的 JS 代码 - 您可以通过打开浏览器的 JS 控制台并更改 Select 小部件的值来查看错误。

至于您的实际任务 - 这并不容易,因为 vbar_stacknot a real glyph function。相反,它只是一个辅助函数,它使用必要的参数调用 vbar 字形函数。您对 p.vbar_stack(...) 的调用实际上变成了

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)

这意味着为了更改 VBar 字形所使用的表达式所使用的列,您必须基本上将该 Bokeh Python 代码转换为 BokehJS JavaScript 代码.

话虽如此,这里是您的代码的工作版本,它应该适用于任意数量的颜色,而不仅仅是两种:

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)