Asparagirl
Asparagirl

Reputation: 256

d3: Nodes + Links to make a multi-generation family tree; how to parse data to draw lines?

I'm working on making a three or four generation family tree in d3.js. You can see the early version here:

http://jsfiddle.net/Asparagirl/uenh4j92/8/

Code:

// People
var nodes = [
    { id: 1, name: "Aaron", x: 50, y: 100, gender: "male", dob: "1900", hasParent: false, hasSpouse: true, spouse1_id: 2 },
    { id: 2, name: "Brina" , x: 400, y: 100, gender: "female", dob: "1900", hasParent: false, hasSpouse: true, spouse1_id: 1 },
    { id: 3, name: "Caden", x: 100, y: 260, gender: "female", dob: "1925", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: false },
    { id: 4, name: "David", x: 200, y: 260, gender: "male", dob: "1930", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: false }, 
    { id: 5, name: "Ewa", x: 320, y: 260, gender: "female", dob: "1935", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: true, spouse_id: 6 },
    { id: 6, name: "Feivel", x: 450, y: 260, gender: "male", dob: "1935", hasParent: false, hasSpouse: true, spouse_id: 5 },
    { id: 7, name: "Gershon", x: 390, y: 370, gender: "male", dob: "1955", hasParent: true, parent1_id: 5, parent2_id: 6, hasSpouse: false }
];
var links = [
    { source: 0, target: 1 }
];

// Make the viewport automatically adjust to max X and Y values for nodes
var max_x = 0;
var max_y = 0;
for (var i=0; i<nodes.length; i++) {
    var temp_x, temp_y;
    var temp_x = nodes[i].x + 200;
    var temp_y = nodes[i].y + 40;
    if ( temp_x >= max_x ) { max_x = temp_x; }
    if ( temp_y >= max_y ) { max_y = temp_y; }
}

// Variables
var width = max_x,
    height = max_y,
    margin = {top: 10, right: 10, bottom: 10, left: 10},
    circleRadius = 20,
    circleStrokeWidth = 3;

// Basic setup
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .attr("id", "visualization")
    .attr("xmlns", "http://www.w3.org/2000/svg");

var elem = svg.selectAll("g")
    .data(nodes)

var elemEnter = elem.enter()
    .append("g")
    .attr("data-name", function(d){ return d.name })
    .attr("data-gender", function(d){ return d.gender })
    .attr("data-dob", function(d){ return d.dob })

// Draw one circle per node
var circle = elemEnter.append("circle")
    .attr("cx", function(d){ return d.x })
    .attr("cy", function(d){ return d.y })
    .attr("r", circleRadius)
    .attr("stroke-width", circleStrokeWidth)
    .attr("class", function(d) {
        var returnGender;
        if (d.gender === "female") { returnGender = "circle female"; } 
        else if (d.gender === "male") { returnGender = "circle male"; }
        else { returnGender = "circle"; }
        return returnGender;
    });

// Add text to the nodes
elemEnter.append("text")
    .attr("dx", function(d){ return (d.x + 28) })
    .attr("dy", function(d){ return d.y - 5 })
    .text(function(d){return d.name})
    .attr("class", "text");

// Add text to the nodes
elemEnter.append("text")
    .attr("dx", function(d){ return (d.x + 28) })
    .attr("dy", function(d){ return d.y + 16 })
    .text(function(d){return "b. " + d.dob})
    .attr("class", "text");


// Add links between nodes
var linksEls = svg.selectAll(".link")
    .data(links)
    .enter()
    // Draw the first line (between the primary couple, nodes 0 and 1)
    .append("line")
    .attr("x1",function(d){ return nodes[d.source].x + circleRadius + circleStrokeWidth; })
    .attr("y1",function(d){ return nodes[d.source].y; })
    .attr("x2",function(d){ return nodes[d.target].x - circleRadius - circleStrokeWidth; })
    .attr("y2",function(d){ return nodes[d.target].y; })
    .attr("class","line");
    // Draw subsequent lines (from each of the children to the couple line's midpoint)
    function drawLines(d){
        var x1 = nodes[d.source].x;
        var y1 = nodes[d.source].y;
        var x2 = nodes[d.target].x;
        var y2 = nodes[d.target].y;
        var childNodes = nodes.filter(function(d){ return ( (d.hasParent===true) && (d.id!=7) ) });
        childNodes.forEach(function(childNode){
             svg.append("line")
                 // This draws from the node *up* to the couple line's midpoint
                .attr("x1",function(d){ return childNode.x; })
                .attr("y1",function(d){ return childNode.y - circleRadius - circleStrokeWidth + 1; })
                .attr("x2",function(d){ return (x1+x2)/2; })
                .attr("y2",function(d){ return (y1+y2)/2; })
                .attr("class","line2");
        })
    }
    linksEls.each(drawLines);

So, this works okay, kinda, for one generation. The problem is that when it comes time for the next generation (Ewa married to Feivel, child is Gershom) we have to figure out how to replicate a structure with a straight-line between partners and line to the child coming down from the mid-point of the parents' couple line. A related problem is that right now, the first couple is recognized as a couple (different line type) only by virtue of them being the first two pieces of data in my nodes list, as opposed to truly being recognized as such by reading the data (i.e. hasSpouse, spouse1_id, etc.).

Thoughts and ideas to make this work better are much appreciated!

Upvotes: 0

Views: 206

Answers (1)

Gilsha
Gilsha

Reputation: 14589

Let all the persons having hasSpouse property value true to have a spouse_id(Instead of spouse1_id or spouse_id) and generate links array from the node array as shown below. couple object is used for preventing redundancy of links like links from 0->1 and 1->0.

var couple = {},
    links = [];
nodes.forEach(function(d, i) {
    if (d.hasSpouse) {
        var link = {};
        link["source"] = i;
        var targetIdx;
        nodes.forEach(function(s, sIdx) {
            if (s.id == d.spouse_id) targetIdx = sIdx;
        });
        link["target"] = targetIdx;
        if (!couple[i + "->" + targetIdx] && !couple[targetIdx + "->" + i]) {
            couple[i + "->" + targetIdx] = true;
            links.push(link);
        }
    }
});

Now, you will need to make a small change in the code for finding child nodes in your drawLines method. Find the subnodes by matching it's parent ids.

function drawLines(d) {
    var src = nodes[d.source];
    var tgt = nodes[d.target];
    var x1 = src.x, y1 = src.y, x2 = tgt.x, y2 = tgt.y;
    var childNodes = nodes.filter(function(d) {
        //Code change
        return ((d.hasParent === true) && (d.parent1_id == src.id && d.parent2_id == tgt.id))
    });
    ......................
}

Here is the updated fiddle

Upvotes: 1

Related Questions