Hierarchical data visualized in a collapsible tree format can grow rapidly and fill the screen with nodes, compromising on readability. Particularly so when a node has hundreds of child nodes. This article proposes a pagination mechanism where a fixed number of nodes are displayed at a time, and the user is allowed to move between previous and subsequent nodes. It describes the implementation of pagination in D3.js.

Hierarchical Data and Visual Clutter

The Tree layout is one of the ways in which hierarchical data is displayed in D3. It represents entities as nodes and their parent-child relationships as tidy node links. A sample tree layout is shown below: Pagination in D3.js: Sample tree layout To produce this layout, we have to specify the relationship along with the data. The following is the data (in JSON) for the above diagram, and it shows the minimum information required to create the correct layout hierarchy.
{
    "name": "Root",
    "children": [
        {
            "name": "Parent 1",
            "children": [
                { "name": "Child 1" },
                { "name": "Child 2" }
            ]
        },
        {
            "name": "Parent 2",
            "children": [
                { "name": "Child 1" },
                { "name": "Child 2" }
            ]
        }
    ]
}
When the data becomes enormous, the nodes in the tree get crowded, and it becomes difficult to distinguish between nodes or to read their labels. Here is an example of what you would have: We can bring the pagination approach to this structure to solve the crowding. This approach restricts the nodes displayed to a limit suitable for the displaying window. By introducing Previous and Next buttons, we can traverse through all the nodes. Then it becomes readable and neat, as seen below:

Implementing Pagination in D3

A constant ‘PAGINATION’ determines how many nodes should be shown on a single page. And the ‘pageNo’ variable is added to all the nodes in the data, calculated from the pagination constant, to indicate on which page the nodes belong. There is another variable ‘page’ added to all parents to indicate the current state of their children.
function reset(d) {
    if (d && d.kids) {// only a parent node, with children are allowed to organize (to avoid leaf node)
        d.page = 1;   // to determine the children of first page
        d.children = [];   // d.children holds the paged children.
        d.kids.forEach(function(d1, i) {  // d.kids contains all the child nodes of a given node.
            d1.pageNo = Math.ceil((i + 1) / PAGINATION); // determines the page of the child node
            if (d.page === d1.pageNo) {  // selects the first page child nodes
                d.children.push(d1); // elements of d.children are drawn as child nodes
            }
            reset(d1);  // recursively, repeat this first page organization for whole tree.
        })
    }
}
Now, draw the tree as explained below:
  • Initialize the PAGINATION constant and data.
  • Execute function reset (as explained above) with data. For a parent:
    • Page is initialized to 1 to represent the first page.
    • d.kids contains all children. Contents of d.children are drawn as children for a node parent.
    • For all child nodes, pageNo is assigned using PAGINATION.
    • If pageNo of a child node is same as the parent’s page, that child node  is added to d.children.
    • The above steps are repeated for all nodes in the tree.
  • Draw the tree using D3.
    • Draw the parent node and its children contained in d.children.
    • Draw the next and previous elements.
  • When Next button of a parent is clicked:
    • Parent’s page is incremented.
    • d.children is refilled with children whose pageNo is same as page of parent.
    • The new structure is redrawn.
  • When Previous button of a parent is clicked:
    • Parent’s page is decremented.
    • d.children is refilled with children whose pageNo is same as page of parent.
    • The new structure is redrawn.

Navigation Controls

In a hierarchy, for each parent, Next and Previous buttons are added around children as follows:
  • For a parent, pagingData is created. The pagingData contains next and previous navigation information.
  • If the parent’s page is greater than 1, it indicates that Previous button is needed.
    • An object representing previous is added to the pagingData array.
    • This previous object contains the id of the parent and page number.
  • If parent’s page is less than maximum page possible (i.e., Math.ceil(p.kids.length / PAGINATION)) by the parent, it indicates that Next button is needed.
    • An object representing next, is added to the pagingData array.
    • This next object contains the id of the parent and page number.
  • The Next and Previous buttons are drawn around the children. The above steps are repeated for all the parents.
The code looks like this:
parents.forEach(function(p) {
    var pagingData = []; // pagingData contains next and previous button.
    if (p.page > 1) { // condition to check whether previous buton be added or not.
        pagingData.push({ // previous button. Holds information which parent and page it belongs
            type: "prev",
            parent: p,
            no: (p.page - 1)
        });
    }
    if (p.page < Math.ceil(p.kids.length / PAGINATION)) { // condition to check whether next button be added or not 
        pagingData.push({ // next button. Holds information which parent and page it belongs
            type: "next",
            parent: p,
            no: (p.page + 1)
        });
    }
}
When the Next button is clicked, the ‘page’ variable of the parent is incremented and the tree layout is redrawn. Similarly, when the Previous button is clicked, the ’page’ variable of the parent gets decremented and the tree layout is redrawn.

Conclusion

An interactive tree without pagination is efficient for small data. As data expands, the normal collapsible tree layout becomes cluttered and unusable. We can address this problem with pagination enhancing the readability and appearance of the layout hierarchy.