Public
Edited
Mar 5
Fork of Simple D3
Insert cell
Insert cell
chart = {
const width = 1200; // uncomment for responsive width
const height = 800;
// Get the data (use sample data if real data not loaded yet)
const hierarchyData = data;
const root = d3.hierarchy(hierarchyData);

// More spacious tree layout
const treeLayout = d3
.tree()
.size([width - 200, height - 300])
.nodeSize([220, 220]) // Increased node spacing
.separation((a, b) => (a.parent === b.parent ? 1.5 : 2.5));

treeLayout(root);

// Create an SVG
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.style("font", "14px Arial, sans-serif");

// Zoom group
const g = svg.append("g").attr("transform", `translate(${width / 2}, 50)`);

// Zoom functionality
svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([0.3, 3])
.on("zoom", (event) => {
g.attr("transform", event.transform);
})
);

// Curved links between nodes
g.append("g")
.attr("fill", "none")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.5)
.attr("stroke-width", 2)
.selectAll("path")
.data(root.links())
.join("path")
.attr(
"d",
d3
.linkVertical()
.x((d) => d.x)
.y((d) => d.y)
);

// Node creation function
const createPersonNode = (group, d) => {
// Container for each person/couple
const container = group.append("g").attr("class", "person-node");

// Background rectangle with rounded corners
container
.append("rect")
.attr("x", -110)
.attr("y", -60)
.attr("width", 220)
.attr("height", 120)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", d.data.spouse ? "#f0f0f0" : "#e6f2ff")
.attr("stroke", "#666")
.attr("stroke-width", 1.5);

// Name styling function
const formatName = (name) => {
// Split name into parts and capitalize first letters
return name
.split(" ")
.map(
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
)
.join(" ");
};

// Truncate long names
const truncateName = (name, maxLength = 20) => {
return name.length > maxLength
? name.substring(0, maxLength) + "..."
: name;
};

// Handling couples
if (d.data.spouse) {
// Left side (primary person)
container
.append("text")
.attr("x", -100)
.attr("y", -30)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.attr("font-size", "0.9em")
.text(truncateName(formatName(d.data.name)))
.clone(true)
.attr("y", -10)
.attr("font-size", "0.8em")
.attr("fill", "#666")
.text(`Age: ${d.data.age}`);

// Right side (spouse)
container
.append("text")
.attr("x", 100)
.attr("y", -30)
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.attr("font-size", "0.9em")
.text(truncateName(formatName(d.data.spouse.name)))
.clone(true)
.attr("y", -10)
.attr("font-size", "0.8em")
.attr("fill", "#666")
.text(`Age: ${d.data.spouse.age}`);

// Dividing line
container
.append("line")
.attr("x1", -110)
.attr("y1", 0)
.attr("x2", 110)
.attr("y2", 0)
.attr("stroke", "#666")
.attr("stroke-dasharray", "4,4");
} else {
// Single person
container
.append("text")
.attr("x", 0)
.attr("y", -30)
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "0.9em")
.text(truncateName(formatName(d.data.name)))
.clone(true)
.attr("y", -10)
.attr("font-size", "0.8em")
.attr("fill", "#666")
.text(`Age: ${d.data.age}`);
}

// Generation indicator
container
.append("text")
.attr("x", -100)
.attr("y", -45)
.attr("font-size", "0.8em")
.attr("fill", "#666")
.text(`Gen: ${d.data.generation || 0}`);
};

// Create nodes
const node = g
.append("g")
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", (d) => `translate(${d.x},${d.y})`)
.each(function (d) {
createPersonNode(d3.select(this), d);
});

// Add tooltips
node
.append("title")
.text(
(d) =>
`Name: ${d.data.name}\nAge: ${d.data.age}\nGeneration: ${
d.data.generation || 0
}`
);

return svg.node();
}
Insert cell
family
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
data = rawFamilyTree
Insert cell
rawFamilyTree = generateFamilyTree(
family.map((person) => ({ name: person.Name, age: person.Age }))
)
Insert cell
// Recursive function to build D3 node
function buildD3Node(person, allPeople) {
const node = {
name: person.name,
age: person.age,
id: person.id
};

if (person.children && person.children.length > 0) {
node.children = person.children
.map((childId) => allPeople[childId])
.filter(Boolean)
.map((child) => buildD3Node(child, allPeople));
}

return node;
}
Insert cell
// Function to process CSV and generate a family tree
function generateFamilyTree(data) {
const people = data.map((person) => ({
id: person.id || `person_${data.indexOf(person)}`,
name: person.name,
age: parseInt(person.age),
lastName: extractLastName(person.name),
parents: [],
children: [],
spouse: null,
generation: 0
}));

// Extract last name from a full name
function extractLastName(fullName) {
const parts = fullName.split(" ");
return parts.length > 1 ? parts[parts.length - 1] : fullName;
}

// Sort by age (descending) to help identify generations
people.sort((a, b) => b.age - a.age);

// Infer parent-child relationships based on age and last names
inferFamilyRelationships(people);

// Find the root nodes (oldest generation with no detected parents)
const rootPeople = people.filter((person) => person.parents.length === 0);

// Create the root node for the visualization
const familyTree = {
name: "Family Tree",
children: rootPeople.map((person) => createPersonNode(person, people))
};

return familyTree;
}
Insert cell
function inferFamilyRelationships(people) {
// First, try to identify possible spouses (similar ages, different last names)
for (let i = 0; i < people.length; i++) {
for (let j = i + 1; j < people.length; j++) {
const person1 = people[i];
const person2 = people[j];

// Potential spouses: similar age (within 10 years) and not already matched
if (
Math.abs(person1.age - person2.age) <= 10 &&
!person1.spouse &&
!person2.spouse
) {
// For a more accurate algorithm, we would check other criteria here
// such as marriage records, same addresses, etc.
person1.spouse = person2.id;
person2.spouse = person1.id;
}
}
}

// Now identify parent-child relationships
for (let i = 0; i < people.length; i++) {
for (let j = 0; j < people.length; j++) {
if (i !== j) {
const potentialParent = people[i];
const potentialChild = people[j];

// Age difference should be at least 15 years for parent-child
const ageDifference = potentialParent.age - potentialChild.age;

if (ageDifference >= 15 && ageDifference <= 50) {
// Check last name matches or if the spouse's last name matches
let lastNameMatch =
potentialChild.lastName === potentialParent.lastName;

// Check potential mother with different last name
if (!lastNameMatch && potentialParent.spouse) {
const spouse = people.find((p) => p.id === potentialParent.spouse);
if (spouse && potentialChild.lastName === spouse.lastName) {
lastNameMatch = true;
}
}

if (lastNameMatch) {
// Add parent-child relationship
if (!potentialChild.parents.includes(potentialParent.id)) {
potentialChild.parents.push(potentialParent.id);
potentialParent.children.push(potentialChild.id);
}

// Add spouse as parent too if exists
if (potentialParent.spouse) {
const spouse = people.find(
(p) => p.id === potentialParent.spouse
);
if (spouse && !potentialChild.parents.includes(spouse.id)) {
potentialChild.parents.push(spouse.id);
spouse.children.push(potentialChild.id);
}
}
}
}
}
}
}

// Set generation numbers
assignGenerations(people);
}
Insert cell
function assignGenerations(people) {
// Start with people who have no parents
const rootPeople = people.filter((person) => person.parents.length === 0);

// Queue for breadth-first traversal
const queue = [...rootPeople];

// Process each person
while (queue.length > 0) {
const current = queue.shift();

// Get children
const children = people.filter((p) => current.children.includes(p.id));

for (const child of children) {
// Make sure all parents are processed before assigning generation
const allParentsProcessed = child.parents.every((parentId) => {
const parent = people.find((p) => p.id === parentId);
return parent.generation !== undefined;
});

if (allParentsProcessed) {
// Find max generation of parents
const maxParentGen = Math.max(
...child.parents.map((parentId) => {
const parent = people.find((p) => p.id === parentId);
return parent.generation;
})
);

// Set child's generation to parent's + 1
child.generation = maxParentGen + 1;

// Add child to queue for processing its children
if (!queue.includes(child)) {
queue.push(child);
}
}
}
}
}
Insert cell
function createPersonNode(person, allPeople) {
const node = {
name: person.name,
age: person.age,
id: person.id,
generation: person.generation
};

// Add spouse if exists
if (person.spouse) {
const spouse = allPeople.find((p) => p.id === person.spouse);
if (spouse) {
node.spouse = {
name: spouse.name,
age: spouse.age,
id: spouse.id
};
}
}

// Add children
const children = allPeople
.filter((p) => person.children.includes(p.id))
// Avoid duplicate children (both parents would list the same child)
.filter(
(child, index, self) => index === self.findIndex((c) => c.id === child.id)
);

if (children.length > 0) {
node.children = children.map((child) => createPersonNode(child, allPeople));
}

return node;
}
Insert cell
class Person {
constructor(fullName, age) {
this.fullName = fullName;
this.age = age;
let names = fullName.split(" ");
if (names.length < 3) {
throw new Error("El nombre debe incluir al menos un nombre y dos apellidos.");
}
// Se asume el formato: [primer nombre] [posibles nombres intermedios] [apellido paterno] [apellido materno]
this.firstName = names[0];
this.secondName = names.length > 3 ? names.slice(1, names.length - 2).join(" ") : null;
this.firstSurname = names[names.length - 2]; // Apellido paterno
this.secondSurname = names[names.length - 1]; // Apellido materno

this.parents = [];
this.children = [];
this.siblings = [];
// Propiedades para la visualización
this.generation = undefined;
this.family = undefined;
}

// El candidato es padre si su primer apellido coincide con el primer apellido del hijo
isFatherOf(child) {
let ageDiff = this.age - child.age;
return (ageDiff >= 15 && ageDiff <= 70 && this.firstSurname === child.firstSurname);
}

// El candidato es madre si su primer apellido coincide con el segundo apellido del hijo
isMotherOf(child) {
let ageDiff = this.age - child.age;
return (ageDiff >= 15 && ageDiff <= 70 && this.firstSurname === child.secondSurname);
}

// Dos personas son hermanos si comparten ambos apellidos y la diferencia de edad es ≤ 20 años
isSibling(other) {
return (
this.firstSurname === other.firstSurname &&
this.secondSurname === other.secondSurname &&
Math.abs(this.age - other.age) <= 20
);
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more