Using @p-himik’s suggestion, I rejiggered the last code paste a little bit.
import pandas as pd
from bokeh.models import LinearColorMapper, ColumnDataSource, Slider, Select, CustomJSFilter, CDSView, CustomJS, GroupFilter
from bokeh.layouts import row, column, layout
from bokeh.plotting import figure, output_file, show
from bokeh.palettes import Viridis256, Category20_20
df = pd.read_excel('https://cjdixon.s3-ap-southeast-2.amazonaws.com/bokeh/heatmap_linegraph_datademo.xlsx', index_col=0)
df.reset_index(drop=True, inplace=True)
df = df.astype('object') # float columns were getting converted to Float64Arrays in the js callbacks, which have immutable lengths (push function not available)
df = df.rename(columns={n: str(n) for n in range(1, 11)})
#output_file('heatmap_linegraph.html', title='whatever', mode='inline')
df['period'] = df['period'].astype(str)
periods = df.period.unique().tolist()
causes = df.option.unique().tolist()
abilities = df.ability.unique().tolist()
df['active_column'] = df['5']
source = ColumnDataSource(data=df)
active = 5
color_mapper = LinearColorMapper(palette=Viridis256, low=0, high=0.02)
year_select = Slider(value=active, start=1, end=10, step=1)
ability_select = Select(value='noob', options=['l33t', 'noob'])
ability_filter = CustomJSFilter(args=dict(ability_select=ability_select), code="""
var indices = []
for (var i = 0; i < source.get_length(); i++){
if (source.data['ability'][i] == ability_select.value){
indices.push(true);
} else {
indices.push(false);
}
}
return indices;
""")
view = CDSView(source=source, filters=[ability_filter])
heatmap = figure(x_range=periods, y_range=causes,
x_axis_location="above", sizing_mode="stretch_both")
heatmap_renderer = heatmap.rect(x="period", y="option", width=1, height=0.95,
source=source, view=view,
fill_color={'field': str(active), 'transform': color_mapper},
line_color=None, name=str(active))
line_fig = figure(sizing_mode="stretch_both", y_range=(0, .03), x_range=(0, 60))
line_renderers = []
line_renderer_source_subsets = []
for cause, color in zip(causes, Category20_20):
r = line_fig.line(x='period'
, y=str(active)
, color=color
, line_width=3
, source=ColumnDataSource(data=df[df.option == cause].reset_index(drop=True))
, legend_label=str(cause))
line_renderers.append(r)
# for each line, maintain a subset that is filtered by option but not by ability; this is the "master" source
# for that line, which will then be filtered by ability in the ability callback.
line_renderer_source_subsets.append(ColumnDataSource(data=df[df.option == cause].reset_index(drop=True)))
fields_to_update = list(df.columns.values) + ['index']
ability_select.js_on_change('value', CustomJS(args=dict(source=source, heatmap_source=source,
line_renderers=line_renderers,
line_renderer_source_subsets=line_renderer_source_subsets,
ability_select=ability_select,
fields_to_update=fields_to_update), code="""
// CDSView takes care of heatmap filtering by ability
heatmap_source.change.emit();
// filter lines by ability.
// iterate over all lines:
for (var m = 0; m < line_renderers.length; m++) {
// clear out renderer's old data source
line_renderers[m].data_source.clear();
// iterate over all rows in 'master' data source for this line...
for (var n = 0; n < line_renderer_source_subsets[m].data.index.length; n++) {
// save the ones matching the selected ability...
if (line_renderer_source_subsets[m].data.ability[n] == ability_select.value) {
// and for each field we're tracking, push the field/value combination to this line's data source
for (var f in fields_to_update) {
var field = fields_to_update[f];
line_renderers[m].data_source.data[field].push(line_renderer_source_subsets[m].data[field][n]);
}
}
}
line_renderers[m].data_source.change.emit();
}
"""))
year_select.js_on_change('value', CustomJS(args=dict(heatmap_renderer=heatmap_renderer,
# p=heatmap,
year_select=year_select,
source=source,
# ability_select=ability_select,
line_fig=line_fig,
line_renderers=line_renderers), code="""
const active = cb_obj.value;
const data = heatmap_renderer.data_source.data[active];
heatmap_renderer.name = String(active);
const {transform} = heatmap_renderer.glyph.fill_color;
heatmap_renderer.glyph.fill_color = {field: cb_obj.value, transform: transform};
for (const lr of line_renderers) {
lr.glyph.y = {field: active};
}
source.data['active_column'] = source.data[year_select.value]
source.change.emit()
line_fig.reset.emit()
"""))
top_area = row(year_select, ability_select)
show(layout(column([top_area, heatmap, line_fig]), sizing_mode="stretch_both"))