I have the following data structure:
const data = {
label: "root",
children: [
{
label: "child_1_A",
},
{
label: "child_1_B",
},
{
label: "child_1_C",
},
{
label: "child_1_D",
},
{
label: "child_1_E",
},
{
label: "child_1_F",
children: [
{
label: "child_2_A",
children: [
{ label: "child_3_A" },
{ label: "child_3_B" },
{ label: "child_3_C" },
{
label: "child_3_D",
children: [
{
label: "child_4_A",
children: [
{
label: "child_5_A",
children: [
{
label: "child_6_A",
},
],
},
{
label: "child_5_B",
children: [
{
label: "child_6_B",
},
],
},
{
label: "child_5_C",
},
{
label: "child_5_D",
children: [
{
label: "child_6_C",
children: [
{
label: "child_7_A",
children: [
{
label: "child_8_A",
},
{
label: "child_8_B",
children: [
{
label: "child_9_A",
children: [
{
label: "child_10_A",
children: [
{
label: "child_11_A",
},
{
label: "child_11_B",
children: [
{
label: "child_12_A",
},
],
},
],
},
],
},
],
},
],
},
],
},
{
label: "child_6_D",
},
],
},
{
label: "child_5_E",
children: [
{
label: "child_6_E",
children: [
{
label: "child_7_B",
},
{
label: "child_7_C",
children: [
{
label: "child_8_C",
},
],
},
],
},
],
},
],
},
],
},
],
},
],
},
],
};
I need to render a nested tree diagram with lines which connect all levels:

This works fine for most levels but when I get to a node which has inner nested children the outer line that connects the nodes doesn’t join up:

so in the grab you can see that child_4_A has a number of children and the line that should connect all of those children doesn’t extend far enough down to child_5_E. I’ve tried lots of different things. The code I have to calculate how tall to draw that line is:
const calculateTotalHeight = (nodes, depth = 0) => {
if (!nodes || nodes.length === 0) return 0;
const nodeHeight = 38;
let totalHeight = 0;
// Process all siblings
for (let i = 0; i < nodes.length - 1; i++) {
// Add height for this sibling
totalHeight += nodeHeight;
// Process children for all nodes, not just non-last siblings
if (nodes[i].children && nodes[i].children.length > 0) {
totalHeight += calculateTotalHeight(nodes[i].children, depth + 1);
}
}
return totalHeight;
};
The entire code is here:
const TreeNode = ({ label, children, index, siblingCount }) => {
// Add helper function to calculate total height
const calculateTotalHeight = (nodes, depth = 0) => {
if (!nodes || nodes.length === 0) return 0;
const nodeHeight = 38;
let totalHeight = 0;
// Process all siblings
for (let i = 0; i < nodes.length - 1; i++) {
// Add height for this sibling
totalHeight += nodeHeight;
// Process children for all nodes, not just non-last siblings
if (nodes[i].children && nodes[i].children.length > 0) {
totalHeight += calculateTotalHeight(nodes[i].children, depth + 1);
}
}
return totalHeight;
};
// Helper to generate curved connecting path
const getConnectorPath = () => {
return `
M 0 16
L 10 16
`;
};
const getCurvedConnectorPath = () => {
return `
M 0 -2 L 0 14.08 C 0 18.4541 4.38531 22 8.7594 22V22
`;
};
const isSibling = index !== undefined && siblingCount > 1;
const isLastSibling = index === siblingCount - 1;
return (
<div className="tree-node">
{/* Main node */}
<div className="node-content">
<span className="label">{label}</span>
</div>
{isSibling && !isLastSibling && (
<>
<svg
className="connector-path"
style={{
position: "absolute",
left: 0,
top: 0,
width: "24px",
height: "100%",
transform: "translateX(-9px)",
overflow: "visible",
}}
>
<path
d={getConnectorPath()}
fill="none"
stroke="#ccc"
strokeWidth="1"
/>
</svg>
</>
)}
{isSibling && isLastSibling && (
<svg
className="connector-curved"
style={{
position: "absolute",
left: 0,
top: 0,
width: "24px",
height: "100%",
transform: "translateX(-9px) translateY(-4px)",
overflow: "visible",
}}
>
<path
d={getCurvedConnectorPath()}
fill="none"
stroke="#ccc"
strokeWidth="1"
/>
</svg>
)}
{/* Single child connector */}
{children?.length === 1 && (
<svg
className="connector-in"
style={{
position: "absolute",
left: 0,
top: 0,
width: "24px",
height: "100%",
transform: "translateX(7px) translateY(33px)",
overflow: "visible",
}}
>
<path
d={getCurvedConnectorPath()}
fill="none"
stroke="#ccc"
strokeWidth="1"
/>
</svg>
)}
{children?.length > 1 && (
<svg
className="vertical-line"
style={{
position: "absolute",
left: 0,
top: "33px",
width: "24px",
transform: "translateX(7px) translateY(-1px)",
overflow: "visible",
zIndex: 1,
}}
>
<path
d={`M0 0 V ${calculateTotalHeight(children)}`}
stroke="#ccc"
strokeWidth="1"
fill="none"
/>
</svg>
)}
{children?.length > 0 && (
<>
<div className="children-container">
<div className="children">
{children.map((child, idx) => (
<TreeNode
key={idx}
{...child}
parent={label}
index={idx}
siblingCount={children.length}
/>
))}
</div>
</div>
</>
)}
<style>{`
.tree-node {
position: relative;
}
.node-content {
display: flex;
z-index: 2;
position: relative;
align-items: center;
gap: 6px;
border: 1px solid #ccc;
border-radius: 6px;
padding: 6px 12px;
width: 95%;
}
.label {
font-size: 12px;
}
.children-container {
position: relative;
margin-left: 16px;
padding-top: 6px;
}
.children {
display: flex;
flex-direction: column;
gap: 6px;
}
`}</style>
</div>
);
};
const Diagram = () => {
const data = {
label: "root",
children: [
{
label: "child_1_A",
},
{
label: "child_1_B",
},
{
label: "child_1_C",
},
{
label: "child_1_D",
},
{
label: "child_1_E",
},
{
label: "child_1_F",
children: [
{
label: "child_2_A",
children: [
{ label: "child_3_A" },
{ label: "child_3_B" },
{ label: "child_3_C" },
{
label: "child_3_D",
children: [
{
label: "child_4_A",
children: [
{
label: "child_5_A",
children: [
{
label: "child_6_A",
},
],
},
{
label: "child_5_B",
children: [
{
label: "child_6_B",
},
],
},
{
label: "child_5_C",
},
{
label: "child_5_D",
children: [
{
label: "child_6_C",
children: [
{
label: "child_7_A",
children: [
{
label: "child_8_A",
},
{
label: "child_8_B",
children: [
{
label: "child_9_A",
children: [
{
label: "child_10_A",
children: [
{
label: "child_11_A",
},
{
label: "child_11_B",
children: [
{
label: "child_12_A",
},
],
},
],
},
],
},
],
},
],
},
],
},
{
label: "child_6_D",
},
],
},
{
label: "child_5_E",
children: [
{
label: "child_6_E",
children: [
{
label: "child_7_B",
},
{
label: "child_7_C",
children: [
{
label: "child_8_C",
},
],
},
],
},
],
},
],
},
],
},
],
},
],
},
],
};
return (
<div style={{ padding: "24px", minHeight: "100vh" }}>
<TreeNode {...data} />
</div>
);
};
export default Diagram;
If I remove the -1 from the for loop then every node with a child ends up with a line being drawn to the bottom of the diagram. The code somehow needs to take all nested children in to account but stop drawing the vertical line at the last child.