Update vbar_stack in a standalone html?

Hello,
I have a follow up question for my last one (Ploting Problems with stacked Plot (varea_stack))
So I have a vbar_stack plot in a standalone html file (so bokeh without a server) and I added a checkBox group to switch each stacker of the plot on or off.

My goal is to update the bar plot, that it only shows the checked stackers (or plot it new , with only the checked stackers)

My code to do this looks like this:

from bokeh.layouts import column, row
from bokeh.plotting import figure, output_file, save, show
from bokeh.palettes import brewer, d3
from bokeh.io import export_png
from bokeh.models import DatetimeTickFormatter, Select, Legend, CustomJS, CheckboxGroup, PreText, ColumnDataSource, CDSView, IndexFilter

stackNames = ['Well-BC4205','2nd-BC4205','Infiltration4205','Springs4205','Qanats4205','Imbalance4205','Storage4205','WellBC4ReturnFlow4205 (143)']
stackPlot = figure()
stackPlot.xaxis.formatter = DatetimeTickFormatter()
checkBox = CheckboxGroup(labels=stackNames, active=[0,1])
checkBox.js_on_change('active', CustomJS(args=dict(stackNames=stackNames), code="""
        const stk = [];
        for (const i of this.active) {
            stk.push(stackNames[i]);
        }
        stackNames = stk;
        //stackNames.change.emit();
        console.log('checkbox_group: new stackNames=' + stackNames);
        console.log('checkbox_group: active=' + this.active, this.toString());           
    """))

sp = stackPlot.vbar_stack(stackers=stackNames, x="date", color=d3['Category20'][len(stackNames)], source=tPos)

bundle = row(checkBox, stackPlot)
show(bundle)

so i thought I could just update stackNames in a CustomJS, but with the code above this isn’t working. I can see from the console.log output, that stackNames got changed in the way i wanted it. But it doesnt affect the plot at all.
My source is a pd.DataFrame which looks like this:

                   date  ... WellBC4ReturnFlow4205 (143)
0   2004-09-22 00:00:00  ...                 8369.992188
1   2004-09-22 00:00:00  ...                 8369.992188
2   2004-09-22 00:00:00  ...                 8369.992188
3   2004-09-22 00:00:00  ...                 8369.992188
4   2004-09-22 00:00:00  ...                 8369.992188
..                  ...  ...                         ...
792 2018-08-22 00:59:00  ...                32149.527344
793 2018-09-01 00:25:00  ...                32149.527344
794 2018-09-20 00:59:00  ...                32149.527344
795 2018-09-21 00:59:00  ...                32149.527344
796 2018-09-22 00:59:00  ...                32149.527344
[797 rows x 11 columns]

so except for the first 3 columns, each column name is a stacker in the plot, and represented in the CheckBoxGroup.
What am I doing wrong? Or is it even possible what I try here? A bokeh server is sadly no option for me.

Unfortunately it would be considerably more work to accomplish this, and, in fact, I have never done it or seen it done, so I can really only offer very general information.

The important thing to note is that the Python vbar_stack method does not generate a single glyph like most methods on figure do. Instead, it is a convenience API that generates multiple glyphs, with pre-set stack transforms set up between them. Paraphasing the vbar_stack docstring:

p.vbar_stack(['2016', '2017'], color=['blue', 'red'], source=source)

is equivalent to the following two separate calls:

p.vbar(bottom=stack(),       top=stack('2016'), color='blue', source=source, name='2016')
p.vbar(bottom=stack('2016'), top=stack('2016', '2017'), color='red',  source=source, name='2017')

If you have 10 stacked bars, there will be 10 separate vbar glyphs, and as you can see, the top and bottome values for each vbar is an expression that depends on all the other vbars that come before it. In order to “update” a vbar_stack you would need to tear down all the existing stack relations and recreate them completely from scratch as appropriate for the new arrangement.

It’s possible the BokehJS API could be expanded in future work to make things like this simpler, but that’s not the case at present.

1 Like

Thank you.
That was very helpful. I mean of course i hoped for better news, but this way I can at least calculate, whether it is worth the effort. Ripping the stack apart and reconstruct it again really sounds not easy . so once again: Thanks a lot

1 Like

Hello again.
Last night i had another idea:
Maybe its hard to update the plot, but is it possible within CustomJS to remove the complete Plot/all glyphs, or the figure and create a new one with the stackers I want?
That sounds doable, but i have no idea how to create a new Figure/Glyph in JS :sweat_smile:

FWIW I’ve done this with CustomJS, using d3 (particularly d3.cumsum). It gets complicated but isn’t impossible (and I wouldn’t consider myself a strong JS user by any means).

Your posted code is almost an MRE → if you can post up a csv/snippet of your data source I might have time to help, it’d be good to get an example of how to do this posted/stored publicly here.

stack

2 Likes

@CarlMarx It’s definitely possible in principle, the only hitch is that vbar_stack has not been implemented in the BokehJS API. So you’d need to implement that part yourself. All vbar_stack does is this:

        for kw in double_stack(stackers, "bottom", "top", **kw):
            self.vbar(**kw)

Where the double_stack function, minus all the error checking that you could probably omit for a quick and dirty implementation, is only about ten lines:

bokeh/src/bokeh/plotting/_stack.py at branch-3.5 · bokeh/bokeh · GitHub

It’s probably reasonable to dd vbar_stack to BokehJS directly, just no-one has done it. Please feel free to open a GitHub Issue about it.

All that said, I am interested to see @gmerritt123’s solution as well.

1 Like

@CarlMarx One idea is to use the block glyph where you stack the data in a CustomJS and update the CDS of the block glyph. I have used the block glyph. One can also use the rect glyph but here the reference of x and y is the center of the rectangle.

from bokeh.plotting import figure, output_file, save
from bokeh.models import ColumnDataSource, CustomJS, MultiSelect
from bokeh.layouts import row
from bokeh.transform import factor_cmap, dodge
from bokeh.palettes import GnBu3
import pandas as pd

output_file("stacked_bar_customJS.html")

JS_CODE = '''
    const select_vals = cb_obj.value;

    const year = [];
    const region = [];
    const value = [];
    const stack_y = [];
    
    for (const y of years) {
        var stack_val = 0;
        for (const v of select_vals) {
            const val = data[v][y];
            year.push(y);
            region.push(v);
            value.push(val);
            stack_y.push(stack_val);
            stack_val += val;
        }
    }
    src_plot.data = {
        'year': year,
        'region': region,
        'value': value,
        'stack_y': stack_y
    }
'''

data = {
    'east': {
        '2016': 100,
        '2017': 120,
        '2018': 40
    },
    'west': {
        '2016': 400,
        '2017': 45,
        '2018': 55
    }
}

df = pd.DataFrame.from_dict(data, orient = 'index').stack()
df.name = 'value'
df.index.names = ['region', 'year']
df = df.reset_index()

regions = list(data.keys())
years = df['year'].unique()
select = MultiSelect(options = regions, value = ['east', 'west'], width = 150)

# create y values for initial stacking of bars
df['stack_y'] = df.groupby('year')['value'].cumsum() - df['value']

sub_df = df[df['region'].isin(select.value)]
src_plot = ColumnDataSource(data = sub_df)

p = figure(x_range = years)

# use block glyph to draw bars
# block glyph is anchored lower left, hence use dodge to offset
p.block(
    x = dodge('year', -0.45, range = p.x_range),
    y = 'stack_y',
    height = 'value',
    width = 0.9,
    source = src_plot,
    name = 'block',
    line_color = 'white',
    fill_color = factor_cmap('region', palette=GnBu3, factors=regions),
    legend_group = 'region'
)

callback = CustomJS(
    args = {'src_plot': src_plot, 'years': years, 'data': data},
    code = JS_CODE
)

select.js_on_change('value', callback)

save(row(select,p))
2 Likes