I’m having issues with getting my TreeGraph to render properly.
Issues:
- Lines go to the wrong location
- Circles Render on top of each other
- Spacing is wrong
Is there perhaps a free open source chart/tree library which will allow me to do this on a canvas, and allow me to make each circle clickable
Example Output (tree listed below)

Code and Samples
Tree Type
export default class Tree {
details:any = {};
children:Tree[] = [];
constructor(_details:any, _children:Tree[]){
this.details = _details;
this.children = _children;
}
}
Tree Data
let data = new Tree(
{ id: "XXXXXX1", name: "A", onClick: ()=>{ console.log("A")}},
[
new Tree({ id: "XXXXXX2", name: "B", onClick: ()=>{ console.log("B")}}, [
new Tree({ id: "XXXXXX4", name: "D", onClick: ()=>{ console.log("D")}}, [])
]),
new Tree({ id: "XXXXXX3", name: "C", onClick: ()=>{ console.log("C")}}, [
new Tree({ id: "XXXXXX5", name: "E", onClick: ()=>{ console.log("E")}}, []),
new Tree({ id: "XXXXXX6", name: "F", onClick: ()=>{ console.log("F")}}, []),
new Tree({ id: "XXXXXX7", name: "G", onClick: ()=>{ console.log("G")}}, [])
]),
new Tree({ id: "XXXXXX8", name: "H", onClick: ()=>{ console.log("H")}}, [
new Tree({ id: "XXXXXX9", name: "I", onClick: ()=>{ console.log("I")}}, []),
new Tree({ id: "XXXXX10", name: "J", onClick: ()=>{ console.log("J")}}, []),
new Tree({ id: "XXXXX11", name: "K", onClick: ()=>{ console.log("K")}}, [])
]),
]
);
Renderer Functional Component (ReactJS)
export default function TreeRenderer(props:any){
// Props
const { width, height, data, styleOverrides } = props;
// States
const [ canvasID, setCanvasID ] = useState<string>("");
const canvasRef = useRef<any>(null);
// Functions
function getTreeWidth(root: Tree): number {
const queue: Tree[] = [root];
let maxWidth = 0;
while (queue.length > 0) {
const levelSize = queue.length;
// Update the maximum width if the current level is wider
maxWidth = Math.max(maxWidth, levelSize);
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!; // Non-null assertion for simplicity
// Enqueue the children for the next level
if (node.children) {
queue.push(...node.children);
}
}
}
return maxWidth;
}
function getMaxChildrenCount(node: Tree): number {
if (!node.children || node.children.length === 0) {
return 0;
}
let maxChildrenCount = node.children.length;
for (const child of node.children) {
const childMaxChildrenCount = getMaxChildrenCount(child);
if (childMaxChildrenCount > maxChildrenCount) {
maxChildrenCount = childMaxChildrenCount;
}
}
return maxChildrenCount;
}
// Handles Drawing the lines, and then returns the circles (just doing it like this so the lines are drawn under the circles)
const drawParentAndChild = (
context: any,
parent: Tree,
depth: number,
sibling: number,
parentX: number,
parentY: number
): any[] => {
let circles: any[] = [];
let radius = 18;
let basePaddingY = 18;
let basePaddingX = 18;
// Adjust spacing based on depth
let paddingY = basePaddingY;// + depth * 10;
let paddingX = basePaddingX;// + depth * 10;
let y = depth * (2 * radius + paddingY) + paddingY;
let x = sibling * (2 * radius + paddingX) + paddingX;
let centerX = x + radius;
let centerY = y + radius;
// Draw Line to Parent
if (depth >= 1) {
context.beginPath();
context.moveTo(centerX, centerY);
context.lineTo(parentX, parentY);
context.strokeStyle = '#1E1E1E';
context.stroke();
context.closePath();
}
// various stuff
let fillColor = '';
if (depth === 0 && sibling === 0) {
fillColor = '#1E1E1E';
} else if (parent.details.isActive ?? false) {
fillColor = 'red';
} else {
fillColor = 'green';
}
// reusable font style
const fontFamily = 'Poppins, sans-serif';
const fontSize = 20;
const fontWeight = 'bold';
circles.push({
a: centerX,
b: centerY,
c: radius,
d: 0,
e: 2 * Math.PI,
f: false,
g: fillColor,
h: `${fontWeight} ${fontSize}px ${fontFamily}`,
i: '#FFFFFF',
j: parent.details.name,
k: x + radius / 2 + 2,
l: y + radius + 8,
});
// Draw Children into Array (only draw the lines)
for (let i = 0; i < parent.children.length; i++) {
let ret = drawParentAndChild(context, parent.children[i], depth + 1, sibling + i, centerX, centerY);
circles.push(...(ret as any[]));
}
return circles;
};
// Effects
useEffect(()=>{
// if no data, don't render
if(!data) return;
// Acquire Canvas
let canvas = canvasRef ? canvasRef.current : document.getElementById(canvasID);
// Get Width and Height of Tree
let tWidth = getTreeWidth(data);
let tHeight = getMaxChildrenCount(data);
// Calculate Sizes
let maxWidth = tWidth;
let maxHeight = tHeight;
// Setup Canvas
canvas.width = maxWidth * 2024;
canvas.height = maxHeight * 2024;
// Get Canvas Context
var context = canvas.getContext('2d');
// Clear Canvas
context.fillStyle = '#FFF';
context.fillRect(0, 0, maxWidth, maxHeight);
// Generate Tree Lines and return circles to draw
let circlesAndText = drawParentAndChild(context, data, 0, 0, 18, 18);
for(let circle of circlesAndText){
context.beginPath();
context.arc(circle['a'], circle['b'], circle['c'], circle['d'], circle['e'], circle['f']);
context.fillStyle = circle['g'];
context.fill();
context.lineWidth = 1;
context.strokeStyle = '#1E1E1E';
context.stroke();
context.closePath();
context.font = circle['h'];
context.fillStyle = circle['i'];
context.fillText(circle['j'], circle['k'], circle['l']);
}
// Loop through
// TODO: Go through each circle here and create a click handler, based on the circle['a'] and circle['b']
}, [data]);
// Render
return <canvas className='tree-renderer-element' ref={canvasRef} style={styleOverrides} id={canvasID} width={0} height={0}></canvas>
}