9

I am a noob in web-development. I'm trying to create a tree-like hierarchical company org chart. I tried both google's visualization chart and Mike Bostock's D3 Reingold tree.

I want these features :

  • tree structure : either top-down (google) or left-right (D3)
  • online/dynamic : viewable in browser and able to read data from json (both google & D3), not static visio or ppt diagram
  • collapsible : able to hide subtrees (both)
  • space-adjusting : nodes should fill visible area, to reduce scrolling (only D3)
  • attributes : display name, title & possibly picture (only google)

Above I've marked which tool allows which features, afaik.
I prefer the D3 version because it looks cool.
I can modify the .json to include additional fields (title, url to photo etc.) - here is a sample

My question is - how do I modify the D3 code to display an employee's name, then title in the next line, and maybe picture too ?

Or if that's not feasible - how do I modify the google code to automatically adjust spacing, so that all children of a node are close together, and I don't have to horizontally scroll ?

d-_-b
  • 761
  • 2
  • 11
  • 26
  • I don't know D3.js well enough to answer your question there. I do know the Google Charts API pretty well, though, and... Let's just say that based on the way you've presented your question that changing the spacing on the org chart is not something you want to attempt. The rendered org chart is actually just a very complicated HTML Table with some fancy CSS applied to it. It's not very flexible. It would take some pretty crazy CSS or jQuery voodoo to get it to do anything other than what it already does. – nbering Jun 19 '15 at 02:15
  • @Balrog30 - yes, I want google charts to adjust the spacing automatically - I rephrased the question [here](http://stackoverflow.com/questions/30928769/google-visualization-org-chart-dynamically-reduce-space-between-nodes) with code samples. – d-_-b Jun 19 '15 at 02:29
  • What I mean to say is, in order to do what you're describing you'd be writing something nearly as complex as what the Google Charts API is doing to draw what it is now. I don't know much about D3, but I know it's open source and extensible. Google Charts is not. You're probably better off with D3.js in this case. Google Charts is great and easy to use as long as you're willing to live with what Google is giving you for free. – nbering Jun 19 '15 at 02:38

2 Answers2

26

Here's a quick example. It modifies this example, to add in first name, last name, a title and a picture.

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>

var margin = {top: 20, right: 120, bottom: 20, left: 120},
    width = 960 - margin.right - margin.left,
    height = 300 - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root;

var tree = d3.layout.tree()
    .size([height, width]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    

var data = {
  "fname": "Rachel",
  "lname": "Rogers",
  "title": "CEO",
  "photo": "http://lorempixel.com/60/60/cats/1",
  "children": [{
        "fname": "Bob",
        "lname": "Smith",
        "title": "President",
        "photo": "http://lorempixel.com/60/60/cats/2",
        "children": [{
              "fname": "Mary",
              "lname": "Jane",
              "title": "Vice President",
              "photo": "http://lorempixel.com/60/60/cats/3",
              "children": [{
                "fname": "Bill",
                "lname": "August",
                "title": "Dock Worker",
                "photo": "http://lorempixel.com/60/60/cats/4"
              }, {
                "fname": "Reginald",
                "lname": "Yoyo",
                "title": "Line Assembly",
                "photo": "http://lorempixel.com/60/60/cats/5"
              }]
            }, {
              "fname": "Nathan",
              "lname": "Ringwald",
              "title": "Comptroller",
              "photo": "http://lorempixel.com/60/60/cats/6"
            }]
  }]
}

root = data;
root.x0 = height / 2;
root.y0 = 0;

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

root.children.forEach(collapse);
update(root);

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.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")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
      .on("click", click);

  // add picture
  nodeEnter
    .append('defs')
    .append('pattern')
    .attr('id', function(d,i){
      return 'pic_' + d.fname + d.lname;
    })
    .attr('height',60)
    .attr('width',60)
    .attr('x',0)
    .attr('y',0)
    .append('image')
    .attr('xlink:href',function(d,i){
      return d.photo;
    })
    .attr('height',60)
    .attr('width',60)
    .attr('x',0)
    .attr('y',0);

  nodeEnter.append("circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  var g = nodeEnter.append("g");
  
  g.append("text")
      .attr("x", function(d) { return d.children || d._children ? -35 : 35; })
      .attr("dy", "1.35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.fname + " " + d.lname; })
      .style("fill-opacity", 1e-6);
      
    g.append("text")
      .attr("x", function(d) { return d.children || d._children ? -35 : 35; })
      .attr("dy", "2.5em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.title; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  nodeUpdate.select("circle")
      .attr("r", 30)
      .style("fill", function(d,i){
        return 'url(#pic_' + d.fname + d.lname+')';
      });

  nodeUpdate.selectAll("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", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.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;
  });
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}

</script>

Reversed Direction:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.node {
  cursor: pointer;
}

.node circle {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>

var margin = {top: 20, right: 120, bottom: 20, left: 120},
    width = 960 - margin.right - margin.left,
    height = 300 - margin.top - margin.bottom;

var i = 0,
    duration = 750,
    root;

var tree = d3.layout.tree()
    .size([height, width]);

var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.x, d.y]; });

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.right + margin.left)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    

var data = {
  "fname": "Rachel",
  "lname": "Rogers",
  "title": "CEO",
  "photo": "http://lorempixel.com/60/60/cats/1",
  "children": [{
        "fname": "Bob",
        "lname": "Smith",
        "title": "President",
        "photo": "http://lorempixel.com/60/60/cats/2",
        "children": [{
              "fname": "Mary",
              "lname": "Jane",
              "title": "Vice President",
              "photo": "http://lorempixel.com/60/60/cats/3",
              "children": [{
                "fname": "Bill",
                "lname": "August",
                "title": "Dock Worker",
                "photo": "http://lorempixel.com/60/60/cats/4"
              }, {
                "fname": "Reginald",
                "lname": "Yoyo",
                "title": "Line Assembly",
                "photo": "http://lorempixel.com/60/60/cats/5"
              }]
            }, {
              "fname": "Nathan",
              "lname": "Ringwald",
              "title": "Comptroller",
              "photo": "http://lorempixel.com/60/60/cats/6"
            }]
  }]
}

root = data;
root.x0 = height / 2;
root.y0 = 0;

function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}

root.children.forEach(collapse);
update(root);

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root).reverse(),
      links = tree.links(nodes);

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = d.depth * 180; });

  // Update the nodes…
  var node = svg.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")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; })
      .on("click", click);

  // add picture
  nodeEnter
    .append('defs')
    .append('pattern')
    .attr('id', function(d,i){
      return 'pic_' + d.fname + d.lname;
    })
    .attr('height',60)
    .attr('width',60)
    .attr('x',0)
    .attr('y',0)
    .append('image')
    .attr('xlink:href',function(d,i){
      return d.photo;
    })
    .attr('height',60)
    .attr('width',60)
    .attr('x',0)
    .attr('y',0);

  nodeEnter.append("circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  var g = nodeEnter.append("g");
  
  g.append("text")
      .attr("x", function(d) { return d.children || d._children ? -35 : 35; })
      .attr("dy", "1.35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.fname + " " + d.lname; })
      .style("fill-opacity", 1e-6);
      
    g.append("text")
      .attr("x", function(d) { return d.children || d._children ? -35 : 35; })
      .attr("dy", "2.5em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.title; })
      .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

  nodeUpdate.select("circle")
      .attr("r", 30)
      .style("fill", function(d,i){
        return 'url(#pic_' + d.fname + d.lname+')';
      });

  nodeUpdate.selectAll("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.x + "," + source.y + ")"; })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select("text")
      .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.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;
  });
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
  update(d);
}

</script>
Mark
  • 106,305
  • 20
  • 172
  • 230
  • This is Great ! I have some more requirements - a search box which traverses to & highlights a particular node, and a frame on the right for more info like large photo,email & phone when a node is clicked. I found some search code [here](http://javascript.tutorialhorizon.com/2014/08/28/selecting-a-node-in-d3js-based-upon-the-value-of-its-data/) and this is a test jsfiddle I'm trying to tweak [here](https://jsfiddle.net/dwuntb98/). But it's beyond my knowledge, while probably take couple hours for you. Would you accept, say bitcoin, for working on this? Does this site have private messaging? – d-_-b Jul 03 '15 at 04:00
  • 4
    @sqld-_-ba, sorry, I am not interested in any work-for-hire. – Mark Jul 05 '15 at 14:59
  • @Mark, is there a way we can change this to show Organization chart vertically rather than horizontally. – Learning Feb 18 '16 at 04:41
  • 1
    @Learning, see updated answer, essentially just reverse the `xs` and `ys` – Mark Feb 18 '16 at 14:02
  • 2
    Those images look quite blurry. Do you know how to fix it? – Flame113 Apr 20 '16 at 10:13
  • 2
    I just can't believe the amount of time/effort you put in this answer, I'd have just given a sample json and few short snippets for the OP to do his homework. By the way, I was here looking for a way to make the height of canvas fluid. Can you point me in right direction. – Fr0zenFyr Jun 14 '16 at 06:28
  • 1
    @Flame113 you are right...the images do look quite blurry. I was able to fix this using viewbox. Check this out: http://stackoverflow.com/questions/29442833/svg-image-inside-circle – Ajay Reddy Nov 09 '16 at 13:14
  • @Mark can you please make this reversed direction chart to more customizable way..like we have to edit the name, position. we have to delete the nodes in the tree..we have to add new node to tree – liza Aug 23 '18 at 05:54
0

If you like to create your project with D3js, Just need to use the script codes from this and replaced3.json("/mbostock/raw/4063550/flare.json", function(error, flare) with this :

d3.json("yourJsonFile/jsonFileName.json", function(error, flare) 

The jsonFileName.json is your customized json file like this sample. Also you can insert name of your pictures into the json file and replace the img tag src with it.

Gabriel
  • 1,413
  • 1
  • 18
  • 29
  • I probably wasn't clear earlier.. I already modified the code to read my sample .json instead of the given flare.json. But how do I 'print' the extra fields ? for e.g. in Line 96, I concatenated title & name `text(function(d) { return d.name+' , '+d.title; })` , but how do i add an image src ? I'd prefer each value to be in a new line. – d-_-b Jun 19 '15 at 01:25
  • 1
    D3 is very flexible and you can customize your data as you want. There is a function you could add every html tag such as `img` the `append()` function. Just need to write this code `d3.select("body").append("img")` and you can change the attributes with `attr()`, So update our code like this: `d3.select("body").append("img").attr("src",'srcPath+name')`. There is a point that I have to mention you can create separated tags and change the position of them with `attr("transform","translate("+x+","+y")"` the `x,y` could be any position. You cannot add the `img` tag inside the `svg`. – Gabriel Jun 19 '15 at 10:08
  • 1
    This [link](https://www.dashingd3js.com/table-of-contents) will help you to learn `d3.js`. Enjoy it. – Gabriel Jun 19 '15 at 10:15