tabulator
tabulator copied to clipboard
Filter for tree/nested data that shows parents of maching nodes
This is a follow-up of issue #1562. (I noticed it was active recently when I was trying to solve the same problem as recently discussed, but it's closed, so I hope it's OK to start this issue here.)
I'm using Tabulator 4.8.
This is about rows representing hierarchical data, where the user would want to show leaf nodes (or intermediate notes) that match certain conditions, and show the hierarchical path that leads to them.
I've tried to produce a short example. Let's say we have a hierarchy representing locations that may contain other locations (it could be street, building, floor, room, desk, drawer, ...), and each of these items may have a colour. We now want to find all the yellow items, but also see where they are in this hierarchy:
https://jsfiddle.net/d08jsy6h/

The problem with this type of filtering (as discussed in the recent comments in issue #1562) is that for each row/node, we need to walk through all its descendants to find if any of them match the filter, in which case it should be displayed too.
Since the table is effectively already turning the tree into rows as a depth-first traversal, it would be inefficient to check the descendants every time (the previous parent row would have performed those checks already).
What I've tried to do is to cache the filtered status obtained during the first depth-first search, so that when the Tabulator expansion itself calls that filter again on sub-nodes, the result is obtained faster, without having to go through all the sub-nodes again.
This particular workaround relies on:
- each node having a unique
id, - clearing out the cache at the end of the filtering (
dataFiltered: function() { deepMatchHeaderFilterStatusMap = {}; }
I've tried it with a larger dataset, and it seems to produce decent performance on a tree with about 1500 nodes (depth up to 4 or 5).
This implementation feels like a workaround. I haven't looked into the internal implementation of Tabulator to see if there was a better way to track the rows (here, we need to know about the id field in the data, and we're also exposing the caching object/map to external code).
It would be great to have a feature like this implemented a bit more natively into Tabulator.
let deepMatchHeaderFilterStatusMap = {};
function deepMatchHeaderFilter(headerValue, rowValue, rowData, filterParams) {
// We check if we've already walked through that node (and therefore subtree).
let cachedStatus = deepMatchHeaderFilterStatusMap[rowData.id];
if (cachedStatus != null) {
// If so, we return the cached result.
return cachedStatus;
}
let columnName = filterParams.columnName;
let anyChildMatch = false;
for (let childRow of rowData._children || []) {
// We walk down the tree recursively
let match = deepMatchHeaderFilter(
headerValue,
childRow[columnName],
childRow,
filterParams
);
deepMatchHeaderFilterStatusMap[rowData.id] = match;
if (match) {
anyChildMatch = true;
}
}
// If any child (and therefore any descendant) matched, we return true.
if (anyChildMatch) {
return true;
}
// We run the actual maching test where applicable. This could be a customised function
//(passed in the filterParams, for example).
if (rowValue != null && rowValue.toString().toLowerCase().includes(headerValue.toLowerCase())) {
return true;
}
return false;
}
const columns = [{
title: "ID",
field: "id",
visible: false,
},
{
title: "Location",
field: "location",
headerFilter: "input",
headerFilterFunc: deepMatchHeaderFilter,
headerFilterFuncParams: {
columnName: "location",
},
},
{
title: "Area",
field: "area",
headerFilter: "input",
headerFilterFunc: deepMatchHeaderFilter,
headerFilterFuncParams: {
columnName: "area",
},
},
{
title: "Color",
field: "color",
headerFilter: "input",
headerFilterFunc: deepMatchHeaderFilter,
headerFilterFuncParams: {
columnName: "color",
},
},
];
var table = new Tabulator("#example-table", {
height: "350px", // Fixed height is required for performance
data: rootNodesArr,
dataTree: true,
dataTreeStartExpanded: true,
dataTreeElementColumn: "location",
dataFiltered: function() {
// Once we're done with filtering, we reset the cache status
deepMatchHeaderFilterStatusMap = {};
},
columns: columns,
});
Sample data:
let rootNodesArr = [{
"id": 1,
"location": "Building A",
"_children": [{
"id": 2,
"location": "Ground Floor",
"_children": [
{"id": 3, "location": "Room 001", "color": "yellow"},
{"id": 4, "location": "Room 002", "color": "blue"},
{"id": 5, "location": "Room 003", "color": "yellow"}
]
}, {
"id": 6,
"location": "First Floor",
"color": "yellow",
"_children": [
{"id": 7, "location": "Room 101", "color": "blue"},
{"id": 8, "location": "Room 102", "color": "red"},
{"id": 9, "location": "Room 103", "color": "blue"}
]
}, {
"id": 10,
"location": "Second Floor",
"_children": [
{"id": 11, "location": "Room 201", "color": "red"},
{"id": 12, "location": "Room 202", "color": "yellow"},
{"id": 13, "location": "Room 203",
"_children": [
{"id": 14, "area": "Front", "color": "yellow"},
{"id": 15, "area": "Back", "color": "blue"}
]
}
]
}]
}, {
"id": 16,
"location": "Building B",
"_children": [{
"id": 17,
"location": "Ground Floor",
"_children": [
{"id": 18, "location": "Room 001", "color": "blue"},
{"id": 19, "location": "Room 002", "color": "yellow"},
{"id": 20, "location": "Room 003", "color": "yellow"}
]
}, {
"id": 21,
"location": "First Floor",
"_children": [
{"id": 22, "location": "Room 101", "color": "yellow"},
{"id": 23, "location": "Room 102", "color": "red"},
{"id": 24, "location": "Room 103", "color": "red"}
]
}, {
"id": 25,
"location": "Second Floor",
"_children": [
{"id": 26, "location": "Room 201", "color": "red"},
{"id": 27, "location": "Room 202", "color": "yellow"},
{"id": 28, "location": "Room 203",
"_children": [
{"id": 29, "area": "Front", "color": "blue"},
{"id": 30, "area": "Back", "color": "red"}
]
}
]
}]
}];
Hey @harbulot
Thanks for raising this issue, i will add this to the roadmap for inclusion in the core library.
Cheers
Oli :)
@olifolkerd Would be awesome if you can make it possible to be able to filter also children of Nested Data Trees. Until now only the parent rows are filtered.
Hi @olifolkerd,
It's been a while since we discussed this issue, but I've found a bit of time to clean up some of the workaround I'd found to address this, with a full example. (There were also a few bugs in the code I'd suggested in an earlier comment.)
This is still using Tabulator 4.9 for now, but hopefully at least the application can be of interest. I'll try to see what I can do to migrate to version 5.
- Live demo: https://harbulot.github.io/tabulator-examples/tree-tests-tabulator-4.9/tree-test-1.html
- Code: https://github.com/harbulot/tabulator-examples
If that's of interest, I'm happy to give that code to the core Tabulator project of course, and help/discuss/contribute if I can.
This is an example to illustrate possible solutions to filtering (and filtered selection) in Tabulator trees.
- Clicking on "Example 1" shows the default Tabulator behaviour.
- Clicking on "Example 2" shows the behaviour with extensions.
This rely on custom functions defined in tree-tests-tabulator-4.9/tabulator-custom-tree-extensions.js.
Some of those custom features include:
- Expand / Collapse the entire tree.
- Collapse the entire tree except the selected rows (and the rows leading to those rows).
In terms of filtering, the strategy is to filter the entire tree and allocate a flag to determine what to do when presenting the tree as a table. The values are as follows:
2indicates that it's an actual match,1indicates that is an ancestor row leading to an actual match.
Those values can then be used for styling or global selection.
In "Example 2":
- Header filtering will apply to the entire tree, filtering out the rows that don't match, but leaving the ancestor rows visible to be able to see the matching rows.
- The non-matching ancestor rows are greyed out and not selectable.
- Using the header checkbox to "select all" will only select the rows matching the filters (not the intermediate ancestors, unless they're a direct match too).
Independently of filtering, there is also a "Go To Selection" feature, which will show and scroll to the selected rows (multiple clicks will move to the next rows in case multiple rows are selected).
Here is a sample screenshot:

Once the header filters are cleared:

Just updated for Tabulator 5.1: https://harbulot.github.io/tabulator-examples/tree-tests-tabulator-5.1/tree-test-1.html
This was relatively straightforward, the main differences are:
dataFilteringanddataFilteredare now handled as events instead of callbacks.- There didn't seem to be any "tree" logic in
rowManager.refreshActiveData, so the handler was set tonullinstead:
- table.rowManager.refreshActiveData("tree", false, true);
+ table.rowManager.refreshActiveData(null, false, true);
- I also had to remove the test condition (so it's now refreshing on all
dataFilteredevents):
dataFiltered: function(filters, rows) {
// Here "rows" is an array RowComponents
const table = this;
// if (table.modules.filter.changed) {
/*
When the filter is first changed, we reformat the rows
(useful when clearing filters too).
*/
for (let row of rows) {
row.reformat();
}
// }
/*
We clear the "changed" flag, otherwise it will still be considered
a new changed for all the dataFiltering/dataFiltered calls for
children in the tree.
*/
table.modules.filter.changed = false;
},
https://harbulot.github.io/tabulator-examples/tree-tests-tabulator-5.1/tree-test-1.html
Hi @harbulot could you provide a working link (the one https://harbulot.github.io/tabulator-examples/tree-tests-tabulator-5.1/tree-test-1.html does not work anymore) or, even better, a fiddle / example or so?
And maybe a hint which files you exactly changed where?
Whats the latest state on filtering childnodes of trees? Looking for a solution here also...
Thanks in advance!
@tobiasgraeber Sorry, it looks like I deleted the examples for Tabulator 5.1 when updating for 5.3, probably by mistake. I've re-added them now.
There is also an updated link for version 5.3: https://harbulot.github.io/tabulator-examples/tree-tests-tabulator-5.3/tree-test-1.html
Hi Folks,
I still have the issue with searching / Filtering the child nodes as shown in the below image. Tried to search for "test" secrete name but it wont result in. Kindly let me know any work around?
@kidman Did you try the workaround described in the first message in this issue?
Hi. I have the same issue regarding datatree hierarchy filter. I would use the tabulator headerfilter:'input' feature but this is not working correctly. The problem that the filter working only the characters which are exist in the parent.
Sample table
Filter is working for 'a' or 'n' as this chars are exist in parent 'Parent Node 1' name.
If I would like to filter for 'b' I cannot as this is not exist in parent 'Parent Node 1' name so this filters out all the tree.
I can solve this filtering issue by filtering before the table load in sqlserver but are you planning to fix this issue?
By the way tabulator is an amazing and we love this. So thanks Oli this great tool.
Thanks, Szabolcs