I had to ship an org chart for our team portal last spring. HR wanted photos, titles, and those tricky dotted-line reports. Oh, and it had to print clean for a board deck. Classic, right?
If you want the blow-by-blow on this exact project, I wrote a companion piece that walks through the three approaches step by step — read the full story here.
I tried three routes: Google Charts OrgChart, OrgChart JS by Balkan, and a custom D3 build. If you’re open to a fourth path, the new EJS Chart library ships an org-chart component that slots nicely between quick demos and full-blown custom builds. I used all three on real data. About 620 people. Mixed managers. New hires every week. It got messy. You know what? That’s where you see what holds up.
For quick reference, the official Google chart documentation is available here, and the OrgChart JS product page lives here.
What I needed (and what bit me)
- Fit 600+ nodes, with smooth zoom and pan
- Collapse branches, but keep search fast
- Show dotted-line (matrix) reports
- Photos, titles, and a small badge for location
- Export to PNG and PDF
- Work on Chrome, Safari, iPad, and a stubborn Windows laptop in a kiosk
Now the fun part: how each tool behaved when I pushed it.
Google Charts OrgChart — fast start, small ceiling
Setup was easy. I had a chart on the page in 15 minutes. It looked clean. Collapsing worked. Search was okay with a simple list.
But I hit limits fast. Styling was tight. Dotted-line links? Not really. Printing was okay for a small team, but for 600 nodes, the layout got cramped and fuzzy.
What I liked
- It’s free and simple
- Great for small teams or a quick demo
- Collapse and expand works out of the box
What bugged me
- Hard to style cards (HTML allowed, but still fussy)
- No clean dotted-line support
- Slow with very large data
A tiny snippet I used:
<div id="org"></div>
<script>
google.charts.load('current', {packages:['orgchart']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
const data = new google.visualization.DataTable();
data.addColumn('string', 'Name');
data.addColumn('string', 'Manager');
data.addColumn('string', 'ToolTip');
data.addRows([
[{'v':'1', 'f':'Ava Lee<div style="color:#777">CEO</div>'}, '', ''],
[{'v':'2', 'f':'Sam Patel<div>VP Sales</div>'}, '1', ''],
[{'v':'3', 'f':'Rin Park<div>VP Eng</div>'}, '1', ''],
]);
const chart = new google.visualization.OrgChart(document.getElementById('org'));
chart.draw(data, {allowHtml: true, size: 'large'});
}
</script>
It worked fine for a pilot. Not for my whole company.
For a broader look at how other open-source chart solutions fared in my tests, check out my separate roundup of contenders — I tried a bunch of open-source JavaScript chart tools, here’s what actually worked.
OrgChart JS (Balkan) — polished, paid, and practical
This one surprised me. It felt like it was built for busy folks like me. Templates, export buttons, drag and drop, and real-world stuff like assistant roles and partner lines.
We shipped this path first. It took me one day to set the base chart and two more days to tune badges, photos, and collapse rules. Export to PDF was crisp. It even handled our Hebrew labels right-to-left, which saved me a week.
What I liked
- Looks great out of the box; templates like “olivia” and “ana”
- Easy export to PNG/PDF
- Built-in search is fast; collapse by level works
- Dotted-line and assistant roles felt natural
What bugged me
- It’s paid (not shocking, just a factor)
- My Safari on macOS stuttered with 1,000+ nodes and huge photos
- Custom link styles needed more tinkering than I hoped
Real snippet from my page:
<div id="tree" style="width:100%; height:700px;"></div>
<script src="orgchart.js"></script>
<script>
const nodes = [
{ id: 1, name: "Ava Lee", title: "CEO", photo: "ava.jpg" },
{ id: 2, pid: 1, name: "Sam Patel", title: "VP Sales", photo: "sam.jpg" },
{ id: 3, pid: 1, name: "Rin Park", title: "VP Eng", photo: "rin.jpg" },
// dotted-line relationship (reports to both Sales and Eng)
{ id: 22, pid: 2, name: "Mia Cho", title: "Ops", photo: "mia.jpg" },
{ id: 22_2, pid: 3, mid: 22 } // maps a second link to Mia
];
const chart = new OrgChart(document.getElementById("tree"), {
template: "olivia",
mouseScrool: OrgChart.action.zoom,
nodeBinding: {
field_0: "name",
field_1: "title",
img_0: "photo"
},
collapse: { level: 2 },
enableSearch: true,
tags: {
remote: { template: "olivia" }
}
});
chart.load(nodes);
// simple export button I wired up
document.getElementById("export").onclick = () => chart.exportPNG();
</script>
That dotted-line trick (the extra entry with mid) was my “aha” moment. It looked tidy on screen and still printed well.
D3.js — full control, more work
I love D3 because I can bend it any way I want. And I did. I used d3.hierarchy and d3.tree, then added curved links. I even switched to canvas for a heavy view and kept SVG for labels. Super fast. Super custom.
But it took time. I wrote keyboard nav. I wrote a print layout. I wrote my own “search and reveal” logic. It felt good, but my team had a deadline, so we kept this build as a backup.
If you’re still shopping for visualization libraries, I also put seven of the most popular JavaScript chart libraries head-to-head — here’s what actually worked.
What I liked
- Total control over style, links, and layout
- Canvas mode flew with 2,000 nodes
- Easy to shape custom badges and tooltips
What bugged me
- More code, more testing
- Printing took real work
- Accessibility needed extra care
Here’s the tiny seed of my D3 build:
<svg id="chart" width="1200" height="800"></svg>
<script src="d3.v7.min.js"></script>
<script>
const data = {
name: "Ava Lee",
children: [
{ name: "Sam Patel", children: [{ name: "Mia Cho" }] },
{ name: "Rin Park" }
]
};
const root = d3.hierarchy(data);
const tree = d3.tree().nodeSize([100, 200])(root);
const svg = d3.select("#chart");
const g = svg.append("g").attr("transform", "translate(60,60)");
g.selectAll(".link")
.data(tree.links())
.enter().append("path")
.attr("class", "link")
.attr("fill", "none")
.attr("stroke", "#999")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
const node = g.selectAll(".node")
.data(tree.descendants())
.enter().append("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("rect")
.attr("width", 150)
.attr("height", 50)
.attr("x", -75)
.attr("y", -25)
.attr("rx", 6)
.attr("fill", "#fff")
.attr("stroke", "#ccc");
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.text(d => d.data.name);
</script>
Simple, clear, and yours to shape. But yes, it’s work.
Real week at work: the reorg crunch
We had a reorg drop on a Thursday. New VPs, a new PMO, and a dotted-line mess. I
