What's the cleanest way to collapse DataTable rows?

Good evening

My bokeh dashboard would be a lot punchier if users could hide subsets of the data on demand. I adapted SlickGrid example 5: Collapsing to bokeh (see below or this fiddle). I would like to confirm that I haven’t missed an easier approach…

Thanks,

Jan

<html lang="en">
<head>
<meta charset="UTF-8">
<title>Collapsing DataTable</title>
<**link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.css" **/>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.css" />
<style>
.toggle {
height: 9
px
;
width: 9
px
;
display: inline-block;
}
.toggle.expand {
background: url(
https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/expand.gif
) no-repeat center center;
}
.toggle.collapse {
background: url(
https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/collapse.gif
) no-repeat center center;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-api.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<div class="bk-root" id="bokeh-root"></div>
<script>
**const ****DataTable = Bokeh.require(‘models/widgets/tables/data_table’);
**class **TreeFormatter **extends **Bokeh.Tables.**CellFormatter **{
constructor(provider) {
super();
this.**data **= provider;
}
doFormat(row, cell, value, columnDef, dataContext) {
const
spacer = <span style='display:inline-block;height:1px;width:**${15 * dataContext.**indent**}**px'></span>,
esc = {
’&’
: ‘&amp’, ‘<’: ‘&lt’, ‘"’: ‘&quot’, “’”: ‘&#039},
safe = (value + ‘’).replace(/&<@’/g, c => esc[c]),
next = this.data.getItem(dataContext[DataTable.DTINDEX_NAME] + 1),
toggle = next[DataTable.DTINDEX_NAME] && next.**indent **> dataContext.**indent
? (dataContext._collapsed **? **‘toggle expand’ **: ‘toggle collapse’) : ‘toggle’;
**return ******${spacer}** <span class='**${toggle}**'></span>&nbsp;**${value}**
**}
}
**function **filter_cb(source) {
const collapsed = source.data._collapsed;
**return **source.data.parent.map((q, i, a) => {
**for **(**let **p = q; p !== null; p = a[p]) {
**if **(collapsed[p])
**return false
**}
**return true
**})
}
**const
*****source *= new **Bokeh.ColumnDataSource({
data: {
parent: [null, null, 1, 1, null],
indent: [0, 0, 1, 1, 0],
name: [
‘Alice’
, ‘Bob’, ‘Cheryl’, ‘Diana’, ‘Eric’],
_collapsed: [false, false, false, false, false],
age: [45, 31, 61, 37, 13]
}
}),
***filter ***= **new **Bokeh.CustomJSFilter({code: return (**${*filter_cb*.toString()}**)(source)}),
***view ***= **new **Bokeh.CDSView({source, filters: [filter]}),
***provider ***= **new **DataTable.DataProvider(source, view),
***columns ***=[
**new **Bokeh.Tables.TableColumn({field: ‘name’, title: ‘Name’, formatter: **new **TreeFormatter(provider)}),
**new **Bokeh.Tables.TableColumn({field: ‘age’, title: ‘Age’}),
],
***table ***= **new **Bokeh.Tables.DataTable({source, view, columns, reorderable: false, index_position: null});
**const
*****views ***= Bokeh.Plotting.show(table, #bokeh-root),
[{grid}] = Object.values(views).filter(view => ‘grid’ ****in view);
grid.onClick.subscribe((e, args) => {
if ($(e.target).hasClass(
‘toggle’
)) {
**const **collapsed = provider.getField(args.row, ‘_collapsed’);
**if **(collapsed !== undefined) {
provider.setField(args.row, ‘_collapsed’, !collapsed)
}
e.stopImmediatePropagation()
}
})
</script>
</body>
</html>

Hi Jan,

As I mentioned in the other thread, using BokehJS directly as a pure JS library is somewhat green territory. It's definitely something we would very much like to improve and promote as time goes on, we are just quite resource-constrained at the moment. Any help (reporting issues, fixing issues, documenting best practices or use-cases) is incredibly appreciated. So basically unfortunately I don't have much specific to say about the code below. It looks fairly compact and straightforward, and if it's working for you that's probably the main thing.

Thanks,

Bryan

···

On Jun 13, 2018, at 17:33, Jan Burgy <[email protected]> wrote:

Good evening

My bokeh dashboard would be a lot punchier if users could hide subsets of the data on demand. I adapted SlickGrid example 5: Collapsing to bokeh (see below or this fiddle). I would like to confirm that I haven't missed an easier approach...

Thanks,

Jan

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Collapsing DataTable</title>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.css&quot; />
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.css&quot; />

    <style>
        .toggle {
            height: 9px;
            width: 9px;
            display: inline-block;
        }
        .toggle.expand {
            background: url(https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/expand.gif\) no-repeat center center;
        }
        .toggle.collapse {
            background: url(https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/collapse.gif\) no-repeat center center;
        }
    </style>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.js&quot;&gt;&lt;/script&gt;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-api.min.js&quot;&gt;&lt;/script&gt;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.js&quot;&gt;&lt;/script&gt;
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js&quot;&gt;&lt;/script&gt;
</head>
<body>

<div class="bk-root" id="bokeh-root"></div>

<script>
const DataTable = Bokeh.require('models/widgets/tables/data_table');

class TreeFormatter extends Bokeh.Tables.CellFormatter {
    constructor(provider) {
        super();
        this.data = provider;
    }

    doFormat(row, cell, value, columnDef, dataContext) {
        const
            spacer = `<span style='display:inline-block;height:1px;width:${15 * dataContext.indent}px'></span>`,
            esc = {'&': '&amp', '<': '&lt', '"': '&quot', "'": '&#039'},
            safe = (value + '').replace(/&<@'/g, c => esc[c]),
            next = this.data.getItem(dataContext[DataTable.DTINDEX_NAME] + 1),
            toggle = next[DataTable.DTINDEX_NAME] && next.indent > dataContext.indent
                ? (dataContext._collapsed ? 'toggle expand' : 'toggle collapse') : 'toggle';

        return `\{spacer\} &lt;span class=&#39;{toggle}'></span>&nbsp;${value}`
    }
}

function filter_cb(source) {
    const collapsed = source.data._collapsed;

    return source.data.parent.map((q, i, a) => {
        for (let p = q; p !== null; p = a[p]) {
            if (collapsed[p])
                return false
        }
        return true
    })
}

const
    source = new Bokeh.ColumnDataSource({
        data: {
            parent: [null, null, 1, 1, null],
            indent: [0, 0, 1, 1, 0],
            name: ['Alice', 'Bob', 'Cheryl', 'Diana', 'Eric'],
            _collapsed: [false, false, false, false, false],
            age: [45, 31, 61, 37, 13]
        }
    }),
    filter = new Bokeh.CustomJSFilter({code: `return (${filter_cb.toString()})(source)`}),
    view = new Bokeh.CDSView({source, filters: [filter]}),
    provider = new DataTable.DataProvider(source, view),
    columns =[
        new Bokeh.Tables.TableColumn({field: 'name', title: 'Name', formatter: new TreeFormatter(provider)}),
        new Bokeh.Tables.TableColumn({field: 'age', title: 'Age'}),
    ],
    table = new Bokeh.Tables.DataTable({source, view, columns, reorderable: false, index_position: null});

const
    views = Bokeh.Plotting.show(table, '#bokeh-root'),
    [{grid}] = Object.values(views).filter(view => 'grid' in view);

grid.onClick.subscribe((e, args) => {
    if ($(e.target).hasClass('toggle')) {
        const collapsed = provider.getField(args.row, '_collapsed');
        if (collapsed !== undefined) {
            provider.setField(args.row, '_collapsed', !collapsed)
        }
        e.stopImmediatePropagation()
    }
})
</script>

</body>
</html>

--
You received this message because you are subscribed to the Google Groups "Bokeh Discussion - Public" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/be592c34-22ad-4058-872f-0ac9342b0976%40continuum.io\.
For more options, visit https://groups.google.com/a/continuum.io/d/optout\.

Thank you Bryan. I have introduced helper sub-classes to clean up the implementation. What is missing from the list below in order to contribute back to Bokeh?

  • convert to TypeScript
  • expose to python
  • add test
  • add example
  • document
    Thanks
···

On Wednesday, June 13, 2018 at 8:33:16 PM UTC-4, Jan Burgy wrote:

Good evening

My bokeh dashboard would be a lot punchier if users could hide subsets of the data on demand. I adapted SlickGrid example 5: Collapsing to bokeh (see below or this fiddle). I would like to confirm that I haven’t missed an easier approach…

Thanks,

Jan

<html lang=“en”>
<head>
<meta charset=“UTF-8”>
<title>Collapsing DataTable</title>
<**link rel=“stylesheet” type=“text/css” href=https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.css” **/>
<link rel=“stylesheet” type=“text/css” href=https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.css/>
<style>
.toggle {
height: 9
px
;
width: 9
px
;
display: inline-block;
}
.toggle.expand {
background: url(
https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/expand.gif
) no-repeat center center;
}
.toggle.collapse {
background: url(
https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.0/images/collapse.gif
) no-repeat center center;
}
</style>
<script src=https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh.min.js></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-api.min.js></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/bokeh/0.12.16/bokeh-tables.min.js></script>
<script src=https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js></script>
</head>
<body>
<div class=“bk-root” id=“bokeh-root”></div>
<script>
**const ****DataTable = Bokeh.require(‘models/widgets/tables/data_table’);
**class **TreeFormatter **extends **Bokeh.Tables.**CellFormatter **{
constructor(provider) {
super();
this.**data **= provider;
}
doFormat(row, cell, value, columnDef, dataContext) {
const
spacer = <span style='display:inline-block;height:1px;width:**${15 * dataContext.**indent**}**px'></span>,
esc = {
‘&’
: ‘&amp’, ‘<’: ‘&lt’, ‘"’: ‘&quot’, “'”: ‘&#039},
safe = (value + ‘’).replace(/&<@'/g, c => esc[c]),
next = this.data.getItem(dataContext[DataTable.DTINDEX_NAME] + 1),
toggle = next[DataTable.DTINDEX_NAME] && next.**indent **> dataContext.**indent
? (dataContext._collapsed **? **‘toggle expand’ **: ‘toggle collapse’) : ‘toggle’;
**return ******${spacer}** <span class='**${toggle}**'></span>&nbsp;**${value}**
**}
}
**function **filter_cb(source) {
const collapsed = source.data._collapsed;
**return **source.data.parent.map((q, i, a) => {
**for **(**let **p = q; p !== null; p = a[p]) {
**if **(collapsed[p])
**return false
**}
**return true
**})
}
**const
*****source *= new **Bokeh.ColumnDataSource({
data: {
parent: [null, null, 1, 1, null],
indent: [0, 0, 1, 1, 0],
name: [
‘Alice’
, ‘Bob’, ‘Cheryl’, ‘Diana’, ‘Eric’],
_collapsed: [false, false, false, false, false],
age: [45, 31, 61, 37, 13]
}
}),
***filter ***= **new **Bokeh.CustomJSFilter({code: return (**${*filter_cb*.toString()}**)(source)}),
***view ***= **new **Bokeh.CDSView({source, filters: [filter]}),
***provider ***= **new **DataTable.DataProvider(source, view),
***columns ***=[
**new **Bokeh.Tables.TableColumn({field: ‘name’, title: ‘Name’, formatter: **new **TreeFormatter(provider)}),
**new **Bokeh.Tables.TableColumn({field: ‘age’, title: ‘Age’}),
],
***table ***= **new **Bokeh.Tables.DataTable({source, view, columns, reorderable: false, index_position: null});
**const
*****views ***= Bokeh.Plotting.show(table, #bokeh-root),
[{grid}] = Object.values(views).filter(view => ‘grid’ ****in view);
grid.onClick.subscribe((e, args) => {
if ($(e.target).hasClass(
‘toggle’
)) {
**const **collapsed = provider.getField(args.row, ‘_collapsed’);
**if **(collapsed !== undefined) {
provider.setField(args.row, ‘_collapsed’, !collapsed)
}
e.stopImmediatePropagation()
}
})
</script>
</body>
</html>