Toggling DataTable columns with a separate widget

I am trying to make a DataTable in a Bokeh server with optional columns that can be toggled on/off with a checkbox widget; something like:

image

Where clicking a button in the checkbox widget would add/remove the corresponding column from the table below. I was able to accomplish this example with some simple CustomJS attached to the widget that explicitly changes the columns of the table:

from datetime import date
from random import randint

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import (
        ColumnDataSource,
        DataTable,
        DateFormatter,
        TableColumn,
        CheckboxButtonGroup,
        CustomJS
    )
from bokeh.plotting import curdoc

data = dict(
        dates=[date(2014, 3, i+1) for i in range(10)],
        downloads=[randint(0, 100) for i in range(10)],
    )
source = ColumnDataSource(data)

columns = [
        TableColumn(field="dates", title="Date", formatter=DateFormatter()),
        TableColumn(field="downloads", title="Downloads"),
    ]
data_table = DataTable(source=source, columns=columns, width=400, height=280)

column_choice = CheckboxButtonGroup(
    labels=[
        "Date",
        "Downloads"
    ],
    active=[0, 1],
)
column_choice.js_on_click(
    CustomJS(
        args=dict(
            table=data_table, columns=columns
        ),
        code="""
            var visible_columns = []
            for (var i = 0; i < this.active.length; i++) {
                visible_columns.push(columns[this.active[i]])
            }
            table.columns = visible_columns;
        """,
    )
)

curdoc().add_root(column(column_choice, data_table))

But I notice that on every click of the widget buttons, we get a DeserializationError trying to handle the patch event (though the columns of the table do toggle as desired):

2021-07-13 13:13:16,349 error handling message
 message: Message 'PATCH-DOC' content: {'events': [{'kind': 'ModelChanged', 'model': {'id': '1007'}, 'attr': 'columns', 'new': [{'id': '1004'}]}], 'references': []} 
 error: DeserializationError("Instance(TableColumn) failed to deserialize reference to {'id': '1004'}")
Traceback (most recent call last):
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/server/protocol_handler.py", line 90, in handle
    work = await handler(message, connection)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/server/session.py", line 67, in _needs_document_lock_wrapper
    result = func(self, *args, **kwargs)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/server/session.py", line 261, in _handle_patch
    message.apply_to_document(self.document, self)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in apply_to_document
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/document/document.py", line 1198, in _with_self_as_curdoc
    return f()
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/protocol/messages/patch_doc.py", line 100, in <lambda>
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/document/document.py", line 411, in apply_json_patch
    patched_obj.set_from_json(attr, value, models=references, setter=setter)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/core/has_props.py", line 412, in set_from_json
    descriptor.set_from_json(self, json, models, setter)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/core/property/descriptors.py", line 623, in set_from_json
    return super().set_from_json(obj, self.property.from_json(json, models), models, setter)
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/core/property/container.py", line 70, in from_json
    return self._new_instance([ self.item_type.from_json(item, models) for item in json ])
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/core/property/container.py", line 70, in <listcomp>
    return self._new_instance([ self.item_type.from_json(item, models) for item in json ])
  File "/home/charlesbluca/miniconda3/envs/dask-distributed/lib/python3.8/site-packages/bokeh/core/property/instance.py", line 93, in from_json
    raise DeserializationError(f"{self} failed to deserialize reference to {json}")
bokeh.core.property.bases.DeserializationError: Instance(TableColumn) failed to deserialize reference to {'id': '1004'}

My questions here are:

  • Is there something I should be doing when explicitly setting the columns of the table to prevent an error like this?
  • If not, is there a recommended way to accomplish something like this? My first thought was to make changes to the fields of the CDS instead, but I’m not exactly sure how something like this could be done while also streaming data into the CDS.

For context, this issue is coming up through work on dask/distributed#4614.

Hi @charlesbluca an approach like this is going to run into a couple of problems. Some are rpotentially related to Bokeh protocol limitations or bugs, but more fundamentally, if you want to remove things and then be able to add them back later (e.g. to toggle “visibility”), then you have to keep a copy of the thing you removed stored somewhere for later reference. That’s what is not happening here and what is leading to trouble.

So, a couple of ideas:

There really should be a TableColumn.visible property on so that individual table columns can be shown or hidden just by toggling that property. I guess (surprisingly) that no one has asked about it before, and that it is just an oversight that it is not there already. So, I’d encourage you to file a GitHub Issue to request this feature. Optimistically, I don’t think it would be a ton of work to implement and could be gotten to fairly soon, but of course you’d have to up the minimum Bokeh version in order to take advantage of it once it lands .

In the mean time, what I would try is this: keep all the columns “alive” in the document by storing them on something else. Then when you remove them from the actual UI table, they won’t go away permanently, and you can add them back later. Where can you store them? One place might be on another DataTable that has visible=False. It would basically just exist to be a “bank” of columns for the real visible table. Another option (if you can set version 2.3 as your minimum) is the the DataModel useful for syncing and storing arbitrary user-defined property data.

Actually I missed that you are passing the columns arg to the CustomJS, the probably ought to be sufficient, so it may just be a protocol issue. You could still try my other suggestions, it’s possible the references specifically in CustomJS are not good enough for some reason, but a second hidden table would be. But you could file a bug report issue for this as well.

Thanks for the help @Bryan! Since this work is on Distributed which currently has a lower minimum version, I’m going to try out the hidden DataTable first and see if that resolves things here. In any case, I’ll open up the appropriate feature request / bug report.

which currently has a lower minimum version

Where is that documented? I could not find it in the GitHub repo. FWIW last time I looked I thought distributed could (should) be more aggressive about bumping min versions.

I’m basing this on the fact that Distributed’s requirements implicitly rely on Dask, which has a minimum version of 1.0.0 as specified in its setup.py. I have brought up the potential of bumping Bokeh before for functional reasons, but the minimum version is old enough that we should probably bump for a lot of other reasons.

Just to be clear on this advice, did you mean that we would have an invisible table (let’s say hidden_table) containing all the columns, and then the CustomJS would reference this hidden_table.columns as the columns to add to the visible table? Asking because I tried to replicate the above example using this strategy and still ran into serialization errors:

from datetime import date
from random import randint

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import (
        ColumnDataSource,
        DataTable,
        DateFormatter,
        TableColumn,
        CheckboxButtonGroup,
        CustomJS
    )
from bokeh.plotting import curdoc

data = dict(
        dates=[date(2014, 3, i+1) for i in range(10)],
        downloads=[randint(0, 100) for i in range(10)],
    )
source = ColumnDataSource(data)

columns = [
        TableColumn(field="dates", title="Date", formatter=DateFormatter()),
        TableColumn(field="downloads", title="Downloads"),
    ]
data_table = DataTable(source=source, columns=columns, width=400, height=280)
hidden_table = DataTable(source=source, columns=columns, width=400, height=280, visible=False)

column_choice = CheckboxButtonGroup(
    labels=[
        "Date",
        "Downloads"
    ],
    active=[0, 1],
)
column_choice.js_on_click(
    CustomJS(
        args=dict(
            table=data_table, columns=hidden_table.columns
        ),
        code="""
            var visible_columns = []
            for (var i = 0; i < this.active.length; i++) {
                visible_columns.push(columns[this.active[i]])
            }
            table.columns = visible_columns;
        """,
    )
)

curdoc().add_root(column(column_choice, data_table, hidden_table))

@charlesbluca Yeah that was my suggestion. I think this just a protocol issue, then. Can you file GitHub Issues for the bug (with the first version of your code, which should work) and also for the visible property feature requests?

Unfortunately I don’t have any further great suggestions. You could try targeting the column CSS directly in the JS callback and hiding it that way, but that might fuss with Bokeh’s own layout so it’s definitely try-and-see territory.

That’s alright, I’m sure there are probably alternative solutions for the underlying problem in my Distributed PR (a table with more columns than can fit on a page). I opened up the issues, and am happy to continue discussion there:

Thanks again for the help here!

1 Like

If there’s only a handful of possibilities the clunky-but-doable option is just to have multiple tables configured with different sets of columns, and toggle the visibility of the entire tables.