DataTable with AjaxDataSource scrolls to last row on update

My goal is to have a DataTable scroll to the last row whenever the table data (source) is updated. There is a previous discussion about this here. In that topic there is a minimal example using a ColumnDataSource, which I have replicated in my own example with Flask, and it works. Here is my version of that example,

#!/usr/bin/python
# -*- coding: utf-8 -*-
from flask import Flask
from bokeh.layouts import layout, column
from bokeh.models.widgets import Div, DataTable, TableColumn
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets.buttons import Button
from bokeh.embed import components
import dominate
from dominate.tags import script, meta
from dominate.util import raw

app = Flask(__name__)
PAGE_URL = "/test"

G = {  # globals...
    "dom_doc": None,
}


def _dominate_document(title="pywrapBokeh", bokeh_version='1.3.4'):
    """ Create dominate document, see https://github.com/Knio/dominate
    """
    G["dom_doc"] = dominate.document(title=title)
    with G["dom_doc"].head:
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-api-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js")
        meta(charset="UTF-8")


@app.route(PAGE_URL, methods=['GET'])
def hello():
    _dominate_document()

    x = list(range(1000))
    y = list(range(1000, 2000))
    source = ColumnDataSource(data=dict(x=x, y=y))

    columns = [TableColumn(field="x", title="x"),
               TableColumn(field="y", title="y")]

    dt_table = DataTable(source=source, columns=columns, selectable=True, scroll_to_selection=True)

    button = Button()
    button.js_on_click(CustomJS(args=dict(source=source), code="""
        var last_row_idx = source.get_length();
        if (last_row_idx) source.selected.indices = [last_row_idx - 1];
        console.log("Button", cb_obj, source, (last_row_idx - 1));
        source.selected.indices = [];
    """))

    doc_layout = layout()
    doc_layout.children.append(column(Div(text="""<h1>Hello World!</h1><p>Using an ColumnDataSource.</p>""")))
    doc_layout.children.append(column(button, dt_table))

    _script, _div = components(doc_layout)
    G["dom_doc"].body += raw(_script)
    G["dom_doc"].body += raw(_div)
    return "{}".format(G["dom_doc"])


# Browser URL: http://127.0.0.1:6800/test
app.run(host="0.0.0.0", port=6800, debug=False)

I am trying to get the same behaviour from an AjaxDataSource, but its not working, in either “append” or “replace” mode. Here is a minimal example of that,

#!/usr/bin/python
# -*- coding: utf-8 -*-
import time
from flask import Flask, jsonify, url_for
from bokeh.layouts import layout, column
from bokeh.models.widgets import Div, DataTable, TableColumn
from bokeh.models import AjaxDataSource, CustomJS
from bokeh.models.widgets.buttons import Button
from bokeh.embed import components
import dominate
from dominate.tags import script, meta
from dominate.util import raw

app = Flask(__name__)
PAGE_URL = "/test"
MODE = ["append", "replace"][1]  # change index 0/1 to get different modes

G = {  # globals...
    "dom_doc": None,
    "table": {"a": []},
}


def _dominate_document(title="pywrapBokeh", bokeh_version='1.3.4'):
    """ Create dominate document, see https://github.com/Knio/dominate
    """
    G["dom_doc"] = dominate.document(title=title)
    with G["dom_doc"].head:
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-tables-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://cdn.pydata.org/bokeh/release/bokeh-api-{bokeh_version}.min.js".format(bokeh_version=bokeh_version))
        script(src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js")
        meta(charset="UTF-8")


@app.route(PAGE_URL + "/table", methods=['POST'])
def url__table_data():
    # Ajax URL, add a timestamp to the data stream, either whole table (replace) or last item (append)
    G["table"]["a"].append("{}".format(time.time()))
    if MODE == "append": return jsonify({"a": [G["table"]["a"][-1]]})
    else: return jsonify(G["table"])


@app.route(PAGE_URL, methods=['GET'])
def hello():
    _dominate_document()

    source = AjaxDataSource(data=G["table"], data_url=url_for('url__table_data'), polling_interval=1000, mode=MODE)

    # The GOAL of this callback is to scroll the DataTable to the last row,
    # every time the datatable gets new data...
    callback = CustomJS(args=dict(source=source),
                        code="""
                            var last_row_idx = source.get_length();
                            if (last_row_idx) source.selected.indices = [last_row_idx - 1];
                            console.log("ads", cb_obj, source, (last_row_idx - 1));
                        """)
    source.js_on_change("data", callback)

    columns = [TableColumn(field=c) for c in source.data.keys()]
    dt_table = DataTable(source=source, columns=columns, selectable=True, scroll_to_selection=True)

    button = Button()
    button.js_on_click(CustomJS(args=dict(source=source), code="""
        var last_row_idx = source.get_length();
        if (last_row_idx) source.selected.indices = [last_row_idx - 1];
        console.log("Button", cb_obj, source, (last_row_idx - 1));
    """))

    doc_layout = layout()
    doc_layout.children.append(column(Div(text="""<h1>Hello World!</h1><p>Using an AjaxDataSource.</p>""")))
    doc_layout.children.append(column(button, dt_table))

    _script, _div = components(doc_layout)
    G["dom_doc"].body += raw(_script)
    G["dom_doc"].body += raw(_div)
    return "{}".format(G["dom_doc"])


# Browser URL: http://127.0.0.1:6800/test
app.run(host="0.0.0.0", port=6800, debug=False)

In the Ajax example, there is a callback for when the data changes, and there is also a Button callback (following the example of the ColumnDataSource). Both callbacks are working in that they get called, but neither callback causes the DataTable to scroll to the last row, as it does in the ColumnDataSource example (above). I have verified that when the either of these callbacks are called, the selected indicies in the model are updated (change value). I have also tried sending a source.change.emit() signal, but that also doesn’t cause the table to scroll.

My workaround, based on the previous topic, is to call SlickGrid functions to scroll the table, which I haven’t yet done…

If it is not working specifically only with AjaxDataSource, then that sounds like a bug of some sort, and a gitHub issue (with full information) would be appropriate.