Skip to content

Instantly share code, notes, and snippets.

@d4tocchini
Forked from robschmuecker/README.md
Last active March 16, 2018 16:25
Show Gist options
  • Save d4tocchini/01ee39f2e16781d87e96b9899f87e9fc to your computer and use it in GitHub Desktop.
Save d4tocchini/01ee39f2e16781d87e96b9899f87e9fc to your computer and use it in GitHub Desktop.
Drag and drop, collapsible d3.js Tree with 50,000 nodes

This is an example of a collapsible drag and drop tree implementing slightly modified code from https://gist.github.com/robschmuecker/7880033

This is posted in order to demonstrate a viable answer for a Stack Overflow question http://stackoverflow.com/questions/20539922/has-anyone-produced-a-virtualised-javascript-tree-for-thousands-of-nodes

The only difference between this gist and the one referenced above is that the JSON file has changed. Each node contains 15 children and the depth is 4. Hence over 50,000 nodes.

The performance is fine if not too many of the nodes are present at once in the DOM. The JSON was contructed at http://www.json-generator.com/ with the following markup

{
        user_id: '{{index}}',
        name: '{{firstName}} {{surname}}',
        children: [
            '{{repeat(15)}}',
            {
                user_id: '{{index}}',
                name: '{{firstName}} {{surname}}',
                children: [
                    '{{repeat(15)}}',
                    {
                        user_id: '{{index}}',
                        name: '{{firstName}} {{surname}}',
                        children: [
                    '{{repeat(15)}}',
                    {
                        user_id: '{{index}}',
                        name: '{{firstName}} {{surname}}',
                        children: [
                        '{{repeat(15)}}',
                    {
                        user_id: '{{index}}',
                        name: '{{firstName}} {{surname}}'
                    }
                        ]
                    }
                ]
                    }
                ]
            }
        ]
}

For any help/queries, http://www.robschmuecker.com @robschmuecker or robert.schmuecker at gmail dot com

// Get JSON data
treeJSON = d3.json("manyNodes.json", function(error, treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
// panning variables
var panSpeed = 200;
var panBoundary = 20; // Within 20px from edges will pan when dragging.
// Misc. variables
var i = 0;
var duration = 750;
var root;
// size of the diagram
var viewerWidth = $(document).width();
var viewerHeight = $(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// sort the tree according to the node names
function sortTree() {
tree.sort(function(a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// TODO: Pan function, can be better implemented.
function pan(domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
} else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function() {
pan(domNode, speed, direction);
}, 50);
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
function initiateDrag(d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id;
}).filter(function(d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function(d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
}
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Define the drag listeners for drag/drop behaviour of nodes.
var time = 0
dragListener = d3.behavior.drag()
.on("dragstart", function(d) {
time = performance.now()
if (d == root) {
return;
}
dragStarted = true;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
// it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
})
.on("drag", function(d) {
if ( performance.now() - time <= 100 ) return
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d, domNode);
}
// get coords of mouseEvent relative to svg container to allow for panning
relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(this, 'left');
} else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(this, 'right');
} else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(this, 'up');
} else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(this, 'down');
} else {
try {
clearTimeout(panTimer);
} catch (e) {
}
}
d.x0 += d3.event.dy;
d.y0 += d3.event.dx;
var node = d3.select(this);
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
updateTempConnector();
}).on("dragend", function(d) {
if (d == root) {
return;
}
domNode = this;
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
centerNode(draggingNode);
draggingNode = null;
}
}
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function(d) {
selectedNode = d;
updateTempConnector();
};
var outCircle = function(d) {
selectedNode = null;
updateTempConnector();
};
// Function to update the temporary connector indicating dragging affiliation
var updateTempConnector = function() {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
// have to flip the source coordinates since we did this for the existing connectors on the original tree
data = [{
source: {
x: selectedNode.y0,
y: selectedNode.x0
},
target: {
x: draggingNode.y0,
y: draggingNode.x0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal());
link.exit().remove();
};
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if (d3.event.defaultPrevented) return; // click suppressed
d = toggleChildren(d);
update(d);
centerNode(d);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
// d.y = (d.depth * 500); //500px per level.
});
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill-opacity", 0);
// phantom node to give us mouseover in a radius around it
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2) // change this to zero to hide the target area
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function(node) {
overCircle(node);
})
.on("mouseout", function(node) {
outCircle(node);
});
// Update the text to reflect whether node has children or not.
node.select('text')
.attr("x", function(d) {
return d.children || d._children ? -10 : 10;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Collapse all children of roots children before rendering.
root.children.forEach(function(child){
collapse(child);
});
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
.node {
cursor: pointer;
}
.overlay{
background-color:#EEE;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size:10px;
font-family:sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.templink {
fill: none;
stroke: red;
stroke-width: 3px;
}
.ghostCircle.show{
display:block;
}
.ghostCircle, .activeDrag .ghostCircle{
display: none;
}
</style>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="dndTree.js"></script>
<body>
<div id="tree-container"></div>
</body>
</html>
{
"user_id": 0,
"name": "Root Node",
"children": [
{
"user_id": 0,
"name": "Flossie Hickman",
"children": [
{
"user_id": 0,
"name": "Violet Arnold",
"children": [
{
"user_id": 0,
"name": "Buckner Carrillo",
"children": [
{
"user_id": 0,
"name": "Scott Poole"
},
{
"user_id": 1,
"name": "Travis Compton"
},
{
"user_id": 2,
"name": "Brenda Galloway"
},
{
"user_id": 3,
"name": "Paula Dennis"
},
{
"user_id": 4,
"name": "Deleon Golden"
},
{
"user_id": 5,
"name": "Russell Mcknight"
},
{
"user_id": 6,
"name": "Fischer Serrano"
},
{
"user_id": 7,
"name": "Cochran Hart"
},
{
"user_id": 8,
"name": "Martin Rollins"
},
{
"user_id": 9,
"name": "Karin Lara"
},
{
"user_id": 10,
"name": "Nannie Blanchard"
},
{
"user_id": 11,
"name": "Sharron Stark"
},
{
"user_id": 12,
"name": "Mcgowan Holcomb"
},
{
"user_id": 13,
"name": "Snider Barrett"
},
{
"user_id": 14,
"name": "Janice Atkins"
}
]
},
{
"user_id": 1,
"name": "Payne Mack",
"children": [
{
"user_id": 0,
"name": "Barker Patrick"
},
{
"user_id": 1,
"name": "Guerrero Mathews"
},
{
"user_id": 2,
"name": "Gaines Greer"
},
{
"user_id": 3,
"name": "Estelle Morgan"
},
{
"user_id": 4,
"name": "Pollard Barnes"
},
{
"user_id": 5,
"name": "Alison Shepard"
},
{
"user_id": 6,
"name": "Imogene Hernandez"
},
{
"user_id": 7,
"name": "Rosalinda Morrow"
},
{
"user_id": 8,
"name": "Baxter Robbins"
},
{
"user_id": 9,
"name": "Mercado Stevens"
},
{
"user_id": 10,
"name": "Price Valentine"
},
{
"user_id": 11,
"name": "Mara Richards"
},
{
"user_id": 12,
"name": "Tommie Mcgee"
},
{
"user_id": 13,
"name": "Shepherd Pacheco"
},
{
"user_id": 14,
"name": "Gibson Preston"
}
]
},
{
"user_id": 2,
"name": "Cox Slater",
"children": [
{
"user_id": 0,
"name": "Celia Jensen"
},
{
"user_id": 1,
"name": "Myers Warner"
},
{
"user_id": 2,
"name": "Kane Burns"
},
{
"user_id": 3,
"name": "Karyn Gonzalez"
},
{
"user_id": 4,
"name": "Winnie Perez"
},
{
"user_id": 5,
"name": "Finley Cook"
},
{
"user_id": 6,
"name": "Elizabeth Garrison"
},
{
"user_id": 7,
"name": "Daniel Simmons"
},
{
"user_id": 8,
"name": "Jan Luna"
},
{
"user_id": 9,
"name": "Leonor Sears"
},
{
"user_id": 10,
"name": "Marquita Santana"
},
{
"user_id": 11,
"name": "Boyer Hanson"
},
{
"user_id": 12,
"name": "Brock Hyde"
},
{
"user_id": 13,
"name": "Candice Jefferson"
},
{
"user_id": 14,
"name": "Odonnell Vargas"
}
]
},
{
"user_id": 3,
"name": "Jodi Duran",
"children": [
{
"user_id": 0,
"name": "Carey Quinn"
},
{
"user_id": 1,
"name": "Lilia Vega"
},
{
"user_id": 2,
"name": "Marci Parker"
},
{
"user_id": 3,
"name": "Marguerite Bartlett"
},
{
"user_id": 4,
"name": "Jill Cooper"
},
{
"user_id": 5,
"name": "Bettie Curry"
},
{
"user_id": 6,
"name": "Owens Shannon"
},
{
"user_id": 7,
"name": "Lakeisha Rasmussen"
},
{
"user_id": 8,
"name": "House Glover"
},
{
"user_id": 9,
"name": "Forbes Short"
},
{
"user_id": 10,
"name": "Francis Finley"
},
{
"user_id": 11,
"name": "Darcy Singleton"
},
{
"user_id": 12,
"name": "Terry Villarreal"
},
{
"user_id": 13,
"name": "Graciela Wooten"
},
{
"user_id": 14,
"name": "Malone Dunlap"
}
]
},
{
"user_id": 4,
"name": "Sabrina Harrell",
"children": [
{
"user_id": 0,
"name": "Dalton Norman"
},
{
"user_id": 1,
"name": "Barrett Morales"
},
{
"user_id": 2,
"name": "Daniels Booth"
},
{
"user_id": 3,
"name": "Maxwell Rich"
},
{
"user_id": 4,
"name": "Bennett Washington"
},
{
"user_id": 5,
"name": "Lelia Banks"
},
{
"user_id": 6,
"name": "Georgette Joyce"
},
{
"user_id": 7,
"name": "Kaitlin Irwin"
},
{
"user_id": 8,
"name": "Lacey Case"
},
{
"user_id": 9,
"name": "Reyes Jenkins"
},
{
"user_id": 10,
"name": "Rios Dunn"
},
{
"user_id": 11,
"name": "Walsh Robles"
},
{
"user_id": 12,
"name": "Nieves Ratliff"
},
{
"user_id": 13,
"name": "Alta Brennan"
},
{
"user_id": 14,
"name": "Theresa Gilliam"
}
]
},
{
"user_id": 5,
"name": "Woods Howe",
"children": [
{
"user_id": 0,
"name": "Adkins Mcguire"
},
{
"user_id": 1,
"name": "Blake Watts"
},
{
"user_id": 2,
"name": "Renee Floyd"
},
{
"user_id": 3,
"name": "Decker Riggs"
},
{
"user_id": 4,
"name": "Chen Benton"
},
{
"user_id": 5,
"name": "English Russo"
},
{
"user_id": 6,
"name": "Wood Cohen"
},
{
"user_id": 7,
"name": "Aisha Molina"
},
{
"user_id": 8,
"name": "Vargas Hensley"
},
{
"user_id": 9,
"name": "Rowena Ray"
},
{
"user_id": 10,
"name": "Clay Farmer"
},
{
"user_id": 11,
"name": "Kathryn Langley"
},
{
"user_id": 12,
"name": "Claudia Dominguez"
},
{
"user_id": 13,
"name": "Jeanne White"
},
{
"user_id": 14,
"name": "Mcleod Holden"
}
]
},
{
"user_id": 6,
"name": "Holmes Wiley",
"children": [
{
"user_id": 0,
"name": "Leach Jacobs"
},
{
"user_id": 1,
"name": "Harriett Macdonald"
},
{
"user_id": 2,
"name": "Castro Bridges"
},
{
"user_id": 3,
"name": "Barber Stephens"
},
{
"user_id": 4,
"name": "Burnett Mills"
},
{
"user_id": 5,
"name": "Freida Jacobson"
},
{
"user_id": 6,
"name": "Juliana Norton"
},
{
"user_id": 7,
"name": "Lindsey Peck"
},
{
"user_id": 8,
"name": "Fran Spence"
},
{
"user_id": 9,
"name": "Sargent Hendrix"
},
{
"user_id": 10,
"name": "Marian Bates"
},
{
"user_id": 11,
"name": "Moses Buck"
},
{
"user_id": 12,
"name": "Fisher Whitley"
},
{
"user_id": 13,
"name": "Tammie Salas"
},
{
"user_id": 14,
"name": "Ratliff Wallace"
}
]
},
{
"user_id": 7,
"name": "Mallory Terry",
"children": [
{
"user_id": 0,
"name": "Rena Levine"
},
{
"user_id": 1,
"name": "Karen Leonard"
},
{
"user_id": 2,
"name": "Reese Copeland"
},
{
"user_id": 3,
"name": "Rosemarie Mays"
},
{
"user_id": 4,
"name": "Lydia Cole"
},
{
"user_id": 5,
"name": "Estella Hood"
},
{
"user_id": 6,
"name": "Felicia Bradford"
},
{
"user_id": 7,
"name": "Suzanne Strong"
},
{
"user_id": 8,
"name": "Hannah Parks"
},
{
"user_id": 9,
"name": "Melendez Cruz"
},
{
"user_id": 10,
"name": "Snow Stout"
},
{
"user_id": 11,
"name": "Lillie Guzman"
},
{
"user_id": 12,
"name": "Patrica Dotson"
},
{
"user_id": 13,
"name": "Zimmerman Fields"
},
{
"user_id": 14,
"name": "Ruth Wong"
}
]
},
{
"user_id": 8,
"name": "Eileen Jennings",
"children": [
{
"user_id": 0,
"name": "Alford Kelley"
},
{
"user_id": 1,
"name": "Jaime Drake"
},
{
"user_id": 2,
"name": "Fuentes Frazier"
},
{
"user_id": 3,
"name": "Combs Oliver"
},
{
"user_id": 4,
"name": "Dixie Davenport"
},
{
"user_id": 5,
"name": "Bishop Montgomery"
},
{
"user_id": 6,
"name": "Elisa Rogers"
},
{
"user_id": 7,
"name": "Dillard Gates"
},
{
"user_id": 8,
"name": "Amalia Livingston"
},
{
"user_id": 9,
"name": "Marva Small"
},
{
"user_id": 10,
"name": "Jones Frank"
},
{
"user_id": 11,
"name": "Gonzalez Moss"
},
{
"user_id": 12,
"name": "Albert Rutledge"
},
{
"user_id": 13,
"name": "Janell Burks"
},
{
"user_id": 14,
"name": "Coleen Saunders"
}
]
},
{
"user_id": 9,
"name": "Alana Shepherd",
"children": [
{
"user_id": 0,
"name": "Compton Newman"
},
{
"user_id": 1,
"name": "Chase Shaw"
},
{
"user_id": 2,
"name": "Nancy Ryan"
},
{
"user_id": 3,
"name": "Terra Sawyer"
},
{
"user_id": 4,
"name": "Lang Landry"
},
{
"user_id": 5,
"name": "Cindy Haynes"
},
{
"user_id": 6,
"name": "Elisabeth Mercer"
},
{
"user_id": 7,
"name": "Deirdre Moody"
},
{
"user_id": 8,
"name": "Juliette Houston"
},
{
"user_id": 9,
"name": "Hopkins Knapp"
},
{
"user_id": 10,
"name": "Keith Charles"
},
{
"user_id": 11,
"name": "Rosario Hooper"
},
{
"user_id": 12,
"name": "Hooper Martin"
},
{
"user_id": 13,
"name": "Sullivan Harrington"
},
{
"user_id": 14,
"name": "Melba Lawson"
}
]
},
]
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment