family-chart
Version:
family tree creator and viewer
1,500 lines (1,298 loc) • 183 kB
JavaScript
// https://donatso.github.io/family-chart/ v0.7.0 Copyright 2025 donatso
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3')) :
typeof define === 'function' && define.amd ? define(['d3'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.f3 = factory(global.f3));
}(this, (function (_d3) { 'use strict';
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
}
});
}
n['default'] = e;
return Object.freeze(n);
}
var _d3__namespace = /*#__PURE__*/_interopNamespace(_d3);
var d3 = typeof window === "object" && !!window.d3 ? window.d3 : _d3__namespace;
function sortChildrenWithSpouses(children, datum, data) {
if (!datum.rels.children) return
const spouses = datum.rels.spouses || [];
return children.sort((a, b) => {
const a_p2 = otherParent(a, datum, data) || {};
const b_p2 = otherParent(b, datum, data) || {};
const a_i = spouses.indexOf(a_p2.id);
const b_i = spouses.indexOf(b_p2.id);
if (datum.data.gender === "M") return a_i - b_i
else return b_i - a_i
})
}
function sortAddNewChildren(children) {
return children.sort((a, b) => {
const a_new = a._new_rel_data;
const b_new = b._new_rel_data;
if (a_new && !b_new) return 1
if (!a_new && b_new) return -1
return 0
})
}
function otherParent(d, p1, data) {
return data.find(d0 => (d0.id !== p1.id) && ((d0.id === d.rels.mother) || (d0.id === d.rels.father)))
}
function calculateEnterAndExitPositions(d, entering, exiting) {
d.exiting = exiting;
if (entering) {
if (d.depth === 0 && !d.spouse) {d._x = d.x; d._y = d.y;}
else if (d.spouse) {d._x = d.spouse.x; d._y = d.spouse.y;}
else if (d.is_ancestry) {d._x = d.parent.x; d._y = d.parent.y;}
else {d._x = d.psx; d._y = d.psy;}
} else if (exiting) {
const x = d.x > 0 ? 1 : -1,
y = d.y > 0 ? 1 : -1;
{d._x = d.x+400*x; d._y = d.y+400*y;}
}
}
function toggleRels(tree_datum, hide_rels) {
const
rels = hide_rels ? 'rels' : '_rels',
rels_ = hide_rels ? '_rels' : 'rels';
if (tree_datum.is_ancestry || tree_datum.data.main) {showHideAncestry('father'); showHideAncestry('mother');}
else {showHideChildren();}
function showHideAncestry(rel_type) {
if (!tree_datum.data[rels] || !tree_datum.data[rels][rel_type]) return
if (!tree_datum.data[rels_]) tree_datum.data[rels_] = {};
tree_datum.data[rels_][rel_type] = tree_datum.data[rels][rel_type];
delete tree_datum.data[rels][rel_type];
}
function showHideChildren() {
if (!tree_datum.data[rels] || !tree_datum.data[rels].children) return
const
children = tree_datum.data[rels].children.slice(0),
spouses = tree_datum.spouse ? [tree_datum.spouse] : tree_datum.spouses || [];
[tree_datum, ...spouses].forEach(sp => children.forEach(ch_id => {
if (sp.data[rels].children.includes(ch_id)) {
if (!sp.data[rels_]) sp.data[rels_] = {};
if (!sp.data[rels_].children) sp.data[rels_].children = [];
sp.data[rels_].children.push(ch_id);
sp.data[rels].children.splice(sp.data[rels].children.indexOf(ch_id), 1);
}
}));
}
}
function toggleAllRels(tree_data, hide_rels) {
tree_data.forEach(d => {d.data.hide_rels = hide_rels; toggleRels(d, hide_rels);});
}
function setupSiblings({tree, data_stash, node_separation, sortChildrenFunction}) {
const main = tree.find(d => d.data.main);
const main_father_id = main.data.rels.father;
const main_mother_id = main.data.rels.mother;
const siblings = findSiblings();
const siblings_added = addSiblingsToTree();
positionSiblings();
function findSiblings() {
return data_stash.filter(d => {
if (d.id === main.data.id) return false
if (main_father_id && d.rels.father === main_father_id) return true
if (main_mother_id && d.rels.mother === main_mother_id) return true
return false
})
}
function addSiblingsToTree() {
const siblings_added = [];
for (let i = 0; i < siblings.length; i++) {
const sib = {data: siblings[i], sibling: true};
sib.parents = [];
const father = main.parents.find(d => d.data.id === sib.data.rels.father);
const mother = main.parents.find(d => d.data.id === sib.data.rels.mother);
if (father) sib.parents.push(father);
if (mother) sib.parents.push(mother);
sib.x = undefined; // to be calculated in positionSiblings
sib.y = main.y;
sib.depth = main.depth-1;
tree.push(sib);
siblings_added.push(sib);
}
return siblings_added
}
function positionSiblings() {
const sorted_siblings = [main, ...siblings_added];
if (sortChildrenFunction) sorted_siblings.sort((a, b) => sortChildrenFunction(a.data, b.data)); // first sort by custom function if provided
sorted_siblings.sort((a, b) => {
const a_father = main.parents.find(d => d.data.id === a.data.rels.father);
const a_mother = main.parents.find(d => d.data.id === a.data.rels.mother);
const b_father = main.parents.find(d => d.data.id === b.data.rels.father);
const b_mother = main.parents.find(d => d.data.id === b.data.rels.mother);
// If a doesn't have mother, it should be to the left
if (!a_mother && b_mother) return -1
// If b doesn't have mother, it should be to the left
if (a_mother && !b_mother) return 1
// If a doesn't have father, it should be to the right
if (!a_father && b_father) return 1
// If b doesn't have father, it should be to the right
if (a_father && !b_father) return -1
// If both have same parents or both missing same parent, maintain original order
return 0
});
const main_x = main.x;
const spouses_x = (main.spouses || []).map(d => d.x);
const x_range = d3.extent([main_x, ...spouses_x]);
const main_sorted_index = sorted_siblings.findIndex(d => d.data.id === main.data.id);
for (let i = 0; i < sorted_siblings.length; i++) {
if (i === main_sorted_index) continue
const sib = sorted_siblings[i];
if (i < main_sorted_index) {
sib.x = x_range[0] - node_separation*(main_sorted_index - i);
} else {
sib.x = x_range[1] + node_separation*(i - main_sorted_index);
}
}
}
}
function handlePrivateCards({tree, data_stash, private_cards_config}) {
const private_persons = {};
const condition = private_cards_config.condition;
if (!condition) return console.error('private_cards_config.condition is not set')
tree.forEach(d => {
if (d.data._new_rel_data) return
const is_private = isPrivate(d.data.id);
if (is_private) d.is_private = is_private;
return
});
function isPrivate(d_id) {
const parents_and_spouses_checked = [];
let is_private = false;
checkParentsAndSpouses(d_id);
private_persons[d_id] = is_private;
return is_private
function checkParentsAndSpouses(d_id) {
if (is_private) return
if (private_persons.hasOwnProperty(d_id)) {
is_private = private_persons[d_id];
return is_private
}
const d = data_stash.find(d0 => d0.id === d_id);
if (d._new_rel_data) return
if (condition(d)) {
is_private = true;
return true
}
const rels = d.rels;
[rels.father, rels.mother, ...(rels.spouses || [])].forEach(d0_id => {
if (!d0_id) return
if (parents_and_spouses_checked.includes(d0_id)) return
parents_and_spouses_checked.push(d0_id);
checkParentsAndSpouses(d0_id);
});
}
}
}
function getMaxDepth(d_id, data_stash) {
const datum = data_stash.find(d => d.id === d_id);
const root_ancestry = d3.hierarchy(datum, hierarchyGetterParents);
const root_progeny = d3.hierarchy(datum, hierarchyGetterChildren);
return {
ancestry: root_ancestry.height,
progeny: root_progeny.height
}
function hierarchyGetterChildren(d) {
return [...(d.rels.children || [])]
.map(id => data_stash.find(d => d.id === id))
.filter(d => d && !d._new_rel_data && !d.to_add)
}
function hierarchyGetterParents(d) {
return [d.rels.father, d.rels.mother]
.filter(d => d)
.map(id => data_stash.find(d => d.id === id))
.filter(d => d && !d._new_rel_data && !d.to_add)
}
}
function checkIfRelativesConnectedWithoutPerson(datum, data_stash) {
const r = datum.rels,
r_ids = [r.father, r.mother, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id),
rels_not_to_main = [];
for (let i = 0; i < r_ids.length; i++) {
const line = findPersonLineToMain(data_stash.find(d => d.id === r_ids[i]), [datum]);
if (!line) {rels_not_to_main.push(r_ids[i]); break;}
}
return rels_not_to_main.length === 0;
function findPersonLineToMain(datum, without_persons) {
let line;
if (isM(datum)) line = [datum];
checkIfAnyRelIsMain(datum, [datum]);
return line
function checkIfAnyRelIsMain(d0, history) {
if (line) return
history = [...history, d0];
runAllRels(check);
if (!line) runAllRels(checkRels);
function runAllRels(f) {
if (!d0) return // todo: check why this happens. test: click spouse and add spouse
const r = d0.rels;
[r.father, r.mother, ...(r.spouses || []), ...(r.children || [])]
.filter(d_id => (d_id && ![...without_persons, ...history].find(d => d.id === d_id)))
.forEach(d_id => f(d_id));
}
function check(d_id) {
if (isM(d_id)) line = history;
}
function checkRels(d_id) {
const person = data_stash.find(d => d.id === d_id);
checkIfAnyRelIsMain(person, history);
}
}
}
function isM(d0) {return typeof d0 === 'object' ? d0.id === data_stash[0].id : d0 === data_stash[0].id} // todo: make main more exact
}
function checkIfConnectedToFirstPerson(datum, data_stash) {
const first_person = data_stash[0];
if (datum.id === first_person.id) return true
const rels_checked = [];
let connected = false;
checkRels(datum);
return connected
function checkRels(d0) {
if (connected) return
const r = d0.rels;
const r_ids = [r.father, r.mother, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id);
r_ids.forEach(r_id => {
if (rels_checked.includes(r_id)) return
rels_checked.push(r_id);
const person = data_stash.find(d => d.id === r_id);
if (person.id === first_person.id) connected = true;
else checkRels(person);
});
}
}
function handleLinkRel(updated_datum, link_rel_id, store_data) {
const new_rel_id = updated_datum.id;
store_data.forEach(d => {
if (d.rels.father === new_rel_id) d.rels.father = link_rel_id;
if (d.rels.mother === new_rel_id) d.rels.mother = link_rel_id;
if ((d.rels.spouses || []).includes(new_rel_id)) {
d.rels.spouses = d.rels.spouses.filter(id => id !== new_rel_id);
if (!d.rels.spouses.includes(link_rel_id)) d.rels.spouses.push(link_rel_id);
}
if ((d.rels.children || []).includes(new_rel_id)) {
d.rels.children = d.rels.children.filter(id => id !== new_rel_id);
if (!d.rels.children.includes(link_rel_id)) d.rels.children.push(link_rel_id);
}
});
const link_rel = store_data.find(d => d.id === link_rel_id);
const new_rel = store_data.find(d => d.id === new_rel_id);
(new_rel.rels.children || []).forEach(child_id => {
if (!link_rel.rels.children) link_rel.rels.children = [];
if (!link_rel.rels.children.includes(child_id)) link_rel.rels.children.push(child_id);
});
(new_rel.rels.spouses || []).forEach(spouse_id => {
if (!link_rel.rels.spouses) link_rel.rels.spouses = [];
if (!link_rel.rels.spouses.includes(spouse_id)) link_rel.rels.spouses.push(spouse_id);
});
if (link_rel.rels.father && new_rel.rels.father) console.error('link rel already has father');
if (link_rel.rels.mother && new_rel.rels.mother) console.error('link rel already has mother');
if (new_rel.rels.father) link_rel.rels.father = new_rel.rels.father;
if (new_rel.rels.mother) link_rel.rels.mother = new_rel.rels.mother;
store_data.splice(store_data.findIndex(d => d.id === new_rel_id), 1);
}
function getLinkRelOptions(datum, data) {
const rel_datum = datum._new_rel_data ? data.find(d => d.id === datum._new_rel_data.rel_id) : null;
const ancestry_ids = getAncestry(datum, data);
const progeny_ids = getProgeny(datum, data);
if (datum._new_rel_data && ['son', 'daughter'].includes(datum._new_rel_data.rel_type)) progeny_ids.push(...getProgeny(rel_datum, data));
return data.filter(d => d.id !== datum.id && d.id !== rel_datum?.id && !d._new_rel_data && !d.to_add && !d.unknown)
.filter(d => !ancestry_ids.includes(d.id))
.filter(d => !progeny_ids.includes(d.id))
.filter(d => !(d.rels.spouses || []).includes(datum.id))
function getAncestry(datum, data_stash) {
const ancestry_ids = [];
loopCheck(datum);
return ancestry_ids
function loopCheck(d) {
const parents = [d.rels.father, d.rels.mother];
parents.forEach(p_id => {
if (p_id) {
ancestry_ids.push(p_id);
loopCheck(data_stash.find(d => d.id === p_id));
}
});
}
}
function getProgeny(datum, data_stash) {
const progeny_ids = [];
loopCheck(datum);
return progeny_ids
function loopCheck(d) {
const children = d.rels.children ? [...d.rels.children] : [];
children.forEach(c_id => {
progeny_ids.push(c_id);
loopCheck(data_stash.find(d => d.id === c_id));
});
}
}
}
function createForm({
datum,
store,
fields,
postSubmit,
addRelative,
removeRelative,
deletePerson,
onCancel,
editFirst,
link_existing_rel_config,
getKinshipInfo,
onFormCreation
}) {
const form_creator = {
datum_id: datum.id,
fields: [],
onSubmit: submitFormChanges,
getKinshipInfo: getKinshipInfo,
onFormCreation: onFormCreation
};
if (!datum._new_rel_data) {
form_creator.onDelete = deletePersonWithPostSubmit;
form_creator.addRelative = () => addRelative.activate(datum),
form_creator.addRelativeCancel = () => addRelative.onCancel();
form_creator.addRelativeActive = addRelative.is_active;
form_creator.removeRelative = () => removeRelative.activate(datum),
form_creator.removeRelativeCancel = () => removeRelative.onCancel();
form_creator.removeRelativeActive = removeRelative.is_active;
form_creator.editable = false;
}
if (datum._new_rel_data) {
form_creator.title = datum._new_rel_data.label;
form_creator.new_rel = true;
form_creator.editable = true;
form_creator.onCancel = onCancel;
}
if (datum._new_rel_data || datum.to_add || datum.unknown) {
if (link_existing_rel_config) form_creator.linkExistingRelative = createLinkExistingRelative(datum, store.getData(), link_existing_rel_config);
}
if (form_creator.onDelete) form_creator.can_delete = true;
if (editFirst) form_creator.editable = true;
const childred_added = (datum.rels.children || []).some(c_id => {const child = store.getDatum(c_id); return !child._new_rel_data});
form_creator.gender_field = {
id: 'gender',
type: 'switch',
label: 'Gender',
initial_value: datum.data.gender,
disabled: ['father', 'mother'].some(rel => rel === datum._new_rel_data?.rel_type) || childred_added,
options: [{value: 'M', label: 'Male'}, {value: 'F', label: 'Female'}]
};
fields.forEach(field => {
if (field.type === 'rel_reference') addRelReferenceField(field);
else if (field.type === 'select') addSelectField(field);
else form_creator.fields.push({
id: field.id,
type: field.type,
label: field.label,
initial_value: datum.data[field.id],
});
});
return form_creator
function addRelReferenceField(field) {
if (!field.getRelLabel) console.error('getRelLabel is not set');
if (field.rel_type === 'spouse') {
(datum.rels.spouses || []).forEach(spouse_id => {
const spouse = store.getDatum(spouse_id);
const marriage_date_id = `${field.id}__ref__${spouse_id}`;
form_creator.fields.push({
id: marriage_date_id,
type: 'rel_reference',
label: field.label,
rel_id: spouse_id,
rel_label: field.getRelLabel(spouse),
initial_value: datum.data[marriage_date_id],
});
});
}
}
function addSelectField(field) {
if (!field.optionCreator && !field.options) return console.error('optionCreator or options is not set for field', field)
form_creator.fields.push({
id: field.id,
type: field.type,
label: field.label,
initial_value: datum.data[field.id],
placeholder: field.placeholder,
options: field.options || field.optionCreator(datum),
});
}
function createLinkExistingRelative(datum, data, link_existing_rel_config) {
const obj = {
label: link_existing_rel_config.label,
options: getLinkRelOptions(datum, data)
.map(d => ({value: d.id, label: link_existing_rel_config.linkRelLabel(d)}))
.sort((a, b) => {
if (typeof a.label === 'string' && typeof b.label === 'string') return a.label.localeCompare(b.label)
else return a.label < b.label ? -1 : 1
}),
onSelect: submitLinkExistingRelative
};
return obj
}
function submitFormChanges(e) {
e.preventDefault();
const form_data = new FormData(e.target);
form_data.forEach((v, k) => datum.data[k] = v);
syncRelReference(datum, store.getData());
if (datum.to_add) delete datum.to_add;
if (datum.unknown) delete datum.unknown;
postSubmit();
}
function submitLinkExistingRelative(e) {
const link_rel_id = e.target.value;
postSubmit({link_rel_id: link_rel_id});
}
function deletePersonWithPostSubmit() {
deletePerson();
postSubmit({delete: true});
}
}
function syncRelReference(datum, data_stash) {
Object.keys(datum.data).forEach(k => {
if (k.includes('__ref__')) {
const rel_id = k.split('__ref__')[1];
const rel = data_stash.find(d => d.id === rel_id);
if (!rel) return
const ref_field_id = k.split('__ref__')[0]+'__ref__'+datum.id;
rel.data[ref_field_id] = datum.data[k];
}
});
}
function onDeleteSyncRelReference(datum, data_stash) {
Object.keys(datum.data).forEach(k => {
if (k.includes('__ref__')) {
const rel_id = k.split('__ref__')[1];
const rel = data_stash.find(d => d.id === rel_id);
if (!rel) return
const ref_field_id = k.split('__ref__')[0]+'__ref__'+datum.id;
delete rel.data[ref_field_id];
}
});
}
function moveToAddToAdded(datum, data_stash) {
delete datum.to_add;
return datum
}
function removeToAdd(datum, data_stash) {
deletePerson(datum, data_stash);
return false
}
function deletePerson(datum, data_stash) {
if (!checkIfRelativesConnectedWithoutPerson(datum, data_stash)) {
changeToUnknown();
return {success: true}
} else {
executeDelete();
return {success: true};
}
function executeDelete() {
data_stash.forEach(d => {
for (let k in d.rels) {
if (!d.rels.hasOwnProperty(k)) continue
if (d.rels[k] === datum.id) {
delete d.rels[k];
} else if (Array.isArray(d.rels[k]) && d.rels[k].includes(datum.id)) {
d.rels[k].splice(d.rels[k].findIndex(did => did === datum.id), 1);
}
}
});
onDeleteSyncRelReference(datum, data_stash);
data_stash.splice(data_stash.findIndex(d => d.id === datum.id), 1);
data_stash.forEach(d => {if (d.to_add) deletePerson(d, data_stash);}); // full update of tree
if (data_stash.length === 0) data_stash.push(createTreeDataWithMainNode({}).data[0]);
}
function changeToUnknown() {
onDeleteSyncRelReference(datum, data_stash);
datum.data = {
gender: datum.data.gender,
};
datum.unknown = true;
}
}
function cleanupDataJson(data) {
data.forEach(d => d.to_add ? removeToAdd(d, data) : d);
data.forEach(d => {
delete d.main;
delete d._tgdp;
delete d._tgdp_sp;
delete d.__tgdp_sp;
});
data.forEach(d => {
Object.keys(d).forEach(k => {
if (k[0] === '_') console.error('key starts with _', k);
});
});
return data
}
function removeToAddFromData(data) {
data.forEach(d => d.to_add ? removeToAdd(d, data) : d);
return data
}
function handleRelsOfNewDatum({datum, data_stash, rel_type, rel_datum}) {
if (rel_type === "daughter" || rel_type === "son") addChild(datum);
else if (rel_type === "father" || rel_type === "mother") addParent(datum);
else if (rel_type === "spouse") addSpouse(datum);
function addChild(datum) {
if (datum.data.other_parent) {
addChildToSpouseAndParentToChild(datum.data.other_parent);
delete datum.data.other_parent;
}
datum.rels[rel_datum.data.gender === 'M' ? 'father' : 'mother'] = rel_datum.id;
if (!rel_datum.rels.children) rel_datum.rels.children = [];
rel_datum.rels.children.push(datum.id);
return datum
function addChildToSpouseAndParentToChild(spouse_id) {
if (spouse_id === "_new") spouse_id = addOtherParent().id;
const spouse = data_stash.find(d => d.id === spouse_id);
datum.rels[spouse.data.gender === 'M' ? 'father' : 'mother'] = spouse.id;
if (!spouse.rels.hasOwnProperty('children')) spouse.rels.children = [];
spouse.rels.children.push(datum.id);
function addOtherParent() {
const new_spouse = createNewPersonWithGenderFromRel({rel_type: "spouse", rel_datum});
addSpouse(new_spouse);
addNewPerson({data_stash, datum: new_spouse});
return new_spouse
}
}
}
function addParent(datum) {
const is_father = datum.data.gender === "M",
parent_to_add_id = rel_datum.rels[is_father ? 'father' : 'mother'];
if (parent_to_add_id) removeToAdd(data_stash.find(d => d.id === parent_to_add_id), data_stash);
addNewParent();
function addNewParent() {
rel_datum.rels[is_father ? 'father' : 'mother'] = datum.id;
handleSpouse();
datum.rels.children = [rel_datum.id];
return datum
function handleSpouse() {
const spouse_id = rel_datum.rels[!is_father ? 'father' : 'mother'];
if (!spouse_id) return
const spouse = data_stash.find(d => d.id === spouse_id);
datum.rels.spouses = [spouse_id];
if (!spouse.rels.spouses) spouse.rels.spouses = [];
spouse.rels.spouses.push(datum.id);
return spouse
}
}
}
function addSpouse(datum) {
removeIfToAdd();
if (!rel_datum.rels.spouses) rel_datum.rels.spouses = [];
rel_datum.rels.spouses.push(datum.id);
datum.rels.spouses = [rel_datum.id];
function removeIfToAdd() {
if (!rel_datum.rels.spouses) return
rel_datum.rels.spouses.forEach(spouse_id => {
const spouse = data_stash.find(d => d.id === spouse_id);
if (spouse.to_add) removeToAdd(spouse, data_stash);
});
}
}
}
function createNewPerson({data, rels}) {
return {id: generateUUID(), data: data || {}, rels: rels || {}}
}
function createNewPersonWithGenderFromRel({data, rel_type, rel_datum}) {
const gender = getGenderFromRelative(rel_datum, rel_type);
data = Object.assign(data || {}, {gender});
return createNewPerson({data})
function getGenderFromRelative(rel_datum, rel_type) {
return (["daughter", "mother"].includes(rel_type) || rel_type === "spouse" && rel_datum.data.gender === "M") ? "F" : "M"
}
}
function addNewPerson({data_stash, datum}) {
data_stash.push(datum);
}
function createTreeDataWithMainNode({data, version}) {
return {data: [createNewPerson({data})], version}
}
function addNewPersonAndHandleRels({datum, data_stash, rel_type, rel_datum}) {
addNewPerson({data_stash, datum});
handleRelsOfNewDatum({datum, data_stash, rel_type, rel_datum});
}
function generateUUID() {
var d = new Date().getTime();
var d2 = (performance && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16;
if(d > 0){//Use timestamp until depleted
r = (d + r)%16 | 0;
d = Math.floor(d/16);
} else {//Use microseconds since page-load if supported
r = (d2 + r)%16 | 0;
d2 = Math.floor(d2/16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function manualZoom({amount, svg, transition_time=500}) {
const el_listener = svg.__zoomObj ? svg : svg.parentNode; // if we need listener for svg and html, we will use parent node
const zoom = el_listener.__zoomObj;
d3.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom
.call(zoom.scaleBy, amount);
}
function getCurrentZoom(svg) {
const el_listener = svg.__zoomObj ? svg : svg.parentNode;
const currentTransform = d3.zoomTransform(el_listener);
return currentTransform
}
function zoomTo(svg, zoom_level) {
const el_listener = svg.__zoomObj ? svg : svg.parentNode;
const currentTransform = d3.zoomTransform(el_listener);
manualZoom({amount: zoom_level / currentTransform.k, svg});
}
function isAllRelativeDisplayed(d, data) {
const r = d.data.rels,
all_rels = [r.father, r.mother, ...(r.spouses || []), ...(r.children || [])].filter(v => v);
return all_rels.every(rel_id => data.some(d => d.data.id === rel_id))
}
function handleDuplicateSpouseToggle(tree) {
tree.forEach(d => {
if (!d.spouse) return
const spouse = d.spouse;
if (d.duplicate && spouse.data._tgdp_sp) {
const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id;
if (spouse.data._tgdp_sp[parent_id]?.hasOwnProperty(d.data.id)) {
d._toggle = spouse.data._tgdp_sp[parent_id][d.data.id];
}
}
});
}
function handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others=true) {
const progeny_duplicates = [];
loopChildren(root);
setToggleIds(progeny_duplicates);
function loopChildren(d) {
if (!d.children) return
const p1 = d.data;
const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id));
const children_by_spouse = getChildrenBySpouse(d);
spouses.forEach(p2 => {
if (progeny_duplicates.some(d => d.some(d => checkIfDuplicate([p1, p2], [d.p1, d.p2])))) {
return
}
const duplicates = findDuplicates(d, p1, p2);
if (duplicates.length > 0) {
const all_duplicates = [{d, p1, p2}, ...duplicates];
progeny_duplicates.push(all_duplicates);
assignDuplicateValues(all_duplicates);
handleToggleOff(all_duplicates);
} else {
let parent_id = root === d ? 'main' : d.parent.data.id;
stashTgdpSpouse(d, parent_id, p2);
(children_by_spouse[p2.id] || []).forEach(child => {
loopChildren(child);
});
}
});
}
function assignDuplicateValues(all_duplicates) {
all_duplicates.forEach(({d, p1, p2}, i) => {
if (!d.data._tgdp_sp) d.data._tgdp_sp = {};
let parent_id = root === d ? 'main' : d.parent.data.id;
unstashTgdpSpouse(d, parent_id, p2);
if (!d.data._tgdp_sp[parent_id]) d.data._tgdp_sp[parent_id] = {};
let val = 1;
if (!d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) d.data._tgdp_sp[parent_id][p2.id] = val;
else val = d.data._tgdp_sp[parent_id][p2.id];
all_duplicates[i].val = val;
});
if (on_toggle_one_close_others) {
if (all_duplicates.every(d => d.val < 0)) {
const first_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0];
const {d, p1, p2} = first_duplicate;
const parent_id = root === d ? 'main' : d.parent.data.id;
d.data._tgdp_sp[parent_id][p2.id] = 1;
}
if (all_duplicates.filter(d => d.val > 0).length > 1) {
const latest_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0];
all_duplicates.forEach(dupl => {
if (dupl === latest_duplicate) return
const {d, p1, p2} = dupl;
const parent_id = root === d ? 'main' : d.parent.data.id;
d.data._tgdp_sp[parent_id][p2.id] = -1;
});
}
}
}
function handleToggleOff(all_duplicates) {
all_duplicates.forEach(({d, p1, p2}) => {
const parent_id = root === d ? 'main' : d.parent.data.id;
if (d.data._tgdp_sp[parent_id][p2.id] < 0) {
const children_by_spouse = getChildrenBySpouse(d);
if (children_by_spouse[p2.id]) {
d.children = d.children.filter(c => !children_by_spouse[p2.id].includes(c));
if (d.children.length === 0) delete d.children;
}
}
});
}
function stashTgdpSpouse(d, parent_id, p2) {
if (d.data._tgdp_sp && d.data._tgdp_sp[parent_id] && d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) {
if (!d.data.__tgdp_sp) d.data.__tgdp_sp = {};
if (!d.data.__tgdp_sp[parent_id]) d.data.__tgdp_sp[parent_id] = {};
d.data.__tgdp_sp[parent_id][p2.id] = d.data._tgdp_sp[parent_id][p2.id];
delete d.data._tgdp_sp[parent_id][p2.id];
}
}
function unstashTgdpSpouse(d, parent_id, p2) {
if (d.data.__tgdp_sp && d.data.__tgdp_sp[parent_id] && d.data.__tgdp_sp[parent_id].hasOwnProperty(p2.id)) {
d.data._tgdp_sp[parent_id][p2.id] = d.data.__tgdp_sp[parent_id][p2.id];
delete d.data.__tgdp_sp[parent_id][p2.id];
}
}
function findDuplicates(datum, partner1, partner2) {
const duplicates = [];
checkChildren(root);
return duplicates
function checkChildren(d) {
if (d === datum) return
if (d.children) {
const p1 = d.data;
const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id));
const children_by_spouse = getChildrenBySpouse(d);
spouses.forEach(p2 => {
if (checkIfDuplicate([partner1, partner2], [p1, p2])) {
duplicates.push({d, p1, p2});
} else {
(children_by_spouse[p2.id] || []).forEach(child => {
checkChildren(child);
});
}
});
}
}
}
function checkIfDuplicate(arr1, arr2) {
return arr1.every(d => arr2.some(d0 => d.id === d0.id))
}
function getChildrenBySpouse(d) {
const children_by_spouse = {};
const p1 = d;
(d.children || []).forEach(child => {
const ch_rels = child.data.rels;
const p2_id = ch_rels.father === p1.data.id ? ch_rels.mother : ch_rels.father;
if (!children_by_spouse[p2_id]) children_by_spouse[p2_id] = [];
children_by_spouse[p2_id].push(child);
});
return children_by_spouse
}
function setToggleIds(progeny_duplicates) {
let toggle_id = 0;
progeny_duplicates.forEach(dupl_arr => {
toggle_id = toggle_id+1;
dupl_arr.forEach(d => {
if (!d.d._toggle_id_sp) d.d._toggle_id_sp = {};
d.d._toggle_id_sp[d.p2.id] = toggle_id;
});
});
}
}
function handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others=true) {
const ancestry_duplicates = [];
loopChildren(root);
setToggleIds(ancestry_duplicates);
function loopChildren(d) {
if (d.children) {
if (ancestry_duplicates.some(d0 => d0.includes(d))) {
return
}
const duplicates = findDuplicates(d.children);
if (duplicates.length > 0) {
const all_duplicates = [d, ...duplicates];
ancestry_duplicates.push(all_duplicates);
assignDuplicateValues(all_duplicates);
handleToggleOff(all_duplicates);
} else {
d.children.forEach(child => {
loopChildren(child);
});
}
}
}
function assignDuplicateValues(all_duplicates) {
all_duplicates.forEach(d => {
if (!d.data._tgdp) d.data._tgdp = {};
const parent_id = root === d ? 'main' : d.parent.data.id;
if (!d.data._tgdp[parent_id]) d.data._tgdp[parent_id] = -1;
d._toggle = d.data._tgdp[parent_id];
});
if (on_toggle_one_close_others) {
if (all_duplicates.every(d => d._toggle < 0)) {
const first_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0];
const d= first_duplicate;
const parent_id = root === d ? 'main' : d.parent.data.id;
d.data._tgdp[parent_id] = 1;
}
if (all_duplicates.filter(d => d._toggle > 0).length > 1) {
const latest_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0];
all_duplicates.forEach(dupl => {
if (dupl === latest_duplicate) return
const d = dupl;
const parent_id = root === d ? 'main' : d.parent.data.id;
d.data._tgdp[parent_id] = -1;
});
}
}
}
function handleToggleOff(all_duplicates) {
all_duplicates.forEach(d => {
const parent_id = root === d ? 'main' : d.parent.data.id;
if (d.data._tgdp[parent_id] < 0) delete d.children;
});
}
function findDuplicates(children_1) {
const duplicates = [];
checkChildren(root);
return duplicates
function checkChildren(d) {
if (d.children) {
if (checkIfDuplicate(children_1, d.children)) {
duplicates.push(d);
} else {
d.children.forEach(child => {
checkChildren(child);
});
}
}
}
}
function checkIfDuplicate(arr1, arr2) {
return arr1 !== arr2 && arr1.every(d => arr2.some(d0 => d.data.id === d0.data.id))
}
function setToggleIds(ancestry_duplicates) {
let toggle_id = 0;
ancestry_duplicates.forEach(dupl_arr => {
toggle_id = toggle_id+1;
dupl_arr.forEach(d => {
d._toggle_id = toggle_id;
});
});
}
}
function CalculateTree({
data, main_id=null,
node_separation=250,
level_separation=150,
single_parent_empty_card=true,
is_horizontal=false,
one_level_rels=false,
sortChildrenFunction=undefined,
sortSpousesFunction=undefined,
ancestry_depth=undefined,
progeny_depth=undefined,
show_siblings_of_main=false,
modifyTreeHierarchy=undefined,
private_cards_config=undefined,
duplicate_branch_toggle=false,
on_toggle_one_close_others=true
}) {
if (!data || !data.length) return {data: [], data_stash: [], dim: {width: 0, height: 0}, main_id: null}
if (is_horizontal) [node_separation, level_separation] = [level_separation, node_separation];
const data_stash = single_parent_empty_card ? createRelsToAdd(data) : data;
const main = (main_id !== null && data_stash.find(d => d.id === main_id)) || data_stash[0];
const tree_children = calculateTreePositions(main, 'children', false);
const tree_parents = calculateTreePositions(main, 'parents', true);
data_stash.forEach(d => d.main = d === main);
levelOutEachSide(tree_parents, tree_children);
const tree = mergeSides(tree_parents, tree_children);
setupChildrenAndParents({tree});
setupSpouses({tree, node_separation});
if (show_siblings_of_main && !one_level_rels) setupSiblings({tree, data_stash, node_separation, sortChildrenFunction});
setupProgenyParentsPos({tree});
nodePositioning({tree});
tree.forEach(d => d.all_rels_displayed = isAllRelativeDisplayed(d, tree));
if (private_cards_config) handlePrivateCards({tree, data_stash, private_cards_config});
setupTid({tree});
setupFromTo(tree);
if (duplicate_branch_toggle) handleDuplicateSpouseToggle(tree);
const dim = calculateTreeDim(tree, node_separation, level_separation);
return {data: tree, data_stash, dim, main_id: main.id, is_horizontal}
function calculateTreePositions(datum, rt, is_ancestry) {
const hierarchyGetter = rt === "children" ? hierarchyGetterChildren : hierarchyGetterParents;
const d3_tree = d3.tree().nodeSize([node_separation, level_separation]).separation(separation);
const root = d3.hierarchy(datum, hierarchyGetter);
if (is_ancestry) addSpouseReferences(root);
trimTree(root, is_ancestry);
if (duplicate_branch_toggle) handleDuplicateHierarchy(root, data_stash, is_ancestry);
if (modifyTreeHierarchy) modifyTreeHierarchy(root, is_ancestry);
d3_tree(root);
return root.descendants()
function separation(a, b) {
let offset = 1;
if (!is_ancestry) {
if (!sameParent(a, b)) offset+=.25;
if (!one_level_rels) {
if (someSpouses(a,b)) offset+=offsetOnPartners(a,b);
}
if (sameParent(a, b) && !sameBothParents(a,b)) offset+=.125;
}
return offset
}
function sameParent(a, b) {return a.parent == b.parent}
function sameBothParents(a, b) {return (a.data.rels.father === b.data.rels.father) && (a.data.rels.mother === b.data.rels.mother)}
function hasSpouses(d) {return d.data.rels.spouses && d.data.rels.spouses.length > 0}
function someSpouses(a, b) {return hasSpouses(a) || hasSpouses(b)}
function hierarchyGetterChildren(d) {
const children = [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id));
if (sortChildrenFunction) children.sort(sortChildrenFunction); // first sort by custom function if provided
sortAddNewChildren(children); // then put new children at the end
if (sortSpousesFunction) sortSpousesFunction(d, data_stash);
sortChildrenWithSpouses(children, d, data_stash); // then sort by order of spouses
return children
}
function hierarchyGetterParents(d) {
return [d.rels.father, d.rels.mother]
.filter(d => d).map(id => data_stash.find(d => d.id === id))
}
function offsetOnPartners(a,b) {
return ((a.data.rels.spouses || []).length + (b.data.rels.spouses || []).length)*.5
}
}
function levelOutEachSide(parents, children) {
const mid_diff = (parents[0].x - children[0].x) / 2;
parents.forEach(d => d.x-=mid_diff);
children.forEach(d => d.x+=mid_diff);
}
function mergeSides(parents, children) {
parents.forEach(d => {d.is_ancestry = true;});
parents.forEach(d => d.depth === 1 ? d.parent = children[0] : null);
return [...children, ...parents.slice(1)];
}
function nodePositioning({tree}) {
tree.forEach(d => {
d.y *= (d.is_ancestry ? -1 : 1);
if (is_horizontal) {
const d_x = d.x; d.x = d.y; d.y = d_x;
}
});
}
function setupSpouses({tree, node_separation}) {
for (let i = tree.length; i--;) {
const d = tree[i];
if (!d.is_ancestry && d.data.rels.spouses && d.data.rels.spouses.length > 0){
if (one_level_rels && d.depth > 0) continue
const side = d.data.data.gender === "M" ? -1 : 1; // female on right
d.x += d.data.rels.spouses.length/2*node_separation*side;
d.data.rels.spouses.forEach((sp_id, i) => {
const spouse = {data: data_stash.find(d0 => d0.id === sp_id), added: true};
spouse.x = d.x-(node_separation*(i+1))*side;
spouse.y = d.y;
spouse.sx = i > 0 ? spouse.x : spouse.x + (node_separation/2)*side;
spouse.sy = i > 0 ? spouse.y : spouse.y + (node_separation/2)*side;
spouse.depth = d.depth;
spouse.spouse = d;
if (!d.spouses) d.spouses = [];
d.spouses.push(spouse);
tree.push(spouse);
});
}
if (d.parents && d.parents.length === 2) {
const p1 = d.parents[0],
p2 = d.parents[1],
midd = p1.x - (p1.x - p2.x)/2,
x = (d,sp) => midd + (node_separation/2)*(d.x < sp.x ? 1 : -1);
p2.x = x(p1, p2); p1.x = x(p2, p1);
}
}
}
function setupProgenyParentsPos({tree}) {
tree.forEach(d => {
if (d.is_ancestry) return
if (d.depth === 0) return
if (d.added) return
if (d.sibling) return
const p1 = d.parent;
const p2 = (p1.spouses || []).find(d0 => d0.data.id === d.data.rels.father || d0.data.id === d.data.rels.mother);
if (p1 && p2) {
if (!p1.added && !p2.added) console.error('no added spouse', p1, p2);
const added_spouse = p1.added ? p1 : p2;
setupParentPos(d, added_spouse);
} else if (p1 || p2) {
const parent = p1 || p2;
parent.sx = parent.x;
parent.sy = parent.y;
setupParentPos(d, parent);
}
function setupParentPos(d, p) {
d.psx = !is_horizontal ? p.sx : p.y;
d.psy = !is_horizontal ? p.y : p.sx;
}
});
}
function setupChildrenAndParents({tree}) {
tree.forEach(d0 => {
delete d0.children;
tree.forEach(d1 => {
if (d1.parent === d0) {
if (d1.is_ancestry) {
if (!d0.parents) d0.parents = [];
d0.parents.push(d1);
} else {
if (!d0.children) d0.children = [];
d0.children.push(d1);
}
}
});
});
}
function calculateTreeDim(tree, node_separation, level_separation) {
if (is_horizontal) [node_separation, level_separation] = [level_separation, node_separation];
const w_extent = d3.extent(tree, d => d.x);
const h_extent = d3.extent(tree, d => d.y);
return {
width: w_extent[1] - w_extent[0]+node_separation, height: h_extent[1] - h_extent[0]+level_separation, x_off: -w_extent[0]+node_separation/2, y_off: -h_extent[0]+level_separation/2
}
}
function createRelsToAdd(data) {
const to_add_spouses = [];
for (let i = 0; i < data.length; i++) {
const d = data[i];
if (d.rels.children && d.rels.children.length > 0) {
if (!d.rels.spouses) d.rels.spouses = [];
const is_father = d.data.gender === "M";
let to_add_spouse;
d.rels.children.forEach(d0 => {
const child = data.find(d1 => d1.id === d0);
if (child.rels[is_father ? 'father' : 'mother'] !== d.id) return
if (child.rels[!is_father ? 'father' : 'mother']) return
if (!to_add_spouse) {
to_add_spouse = findOrCreateToAddSpouse(d);
}
to_add_spouse.rels.children.push(child.id);
child.rels[!is_father ? 'father' : 'mother'] = to_add_spouse.id;
});
}
}
to_add_spouses.forEach(d => data.push(d));
return data
function findOrCreateToAddSpouse(d) {
const spouses = d.rels.spouses.map(sp_id => data.find(d0 => d0.id === sp_id));
return spouses.find(sp => sp.to_add) || createToAddSpouse(d)
}
function createToAddSpouse(d) {
const spouse = createNewPerson({
data: {gender: d.data.gender === "M" ? "F" : "M"},
rels: {spouses: [d.id], children: []}
});
spouse.to_add = true;
to_add_spouses.push(spouse);
d.rels.spouses.push(spouse.id);
return spouse
}
}
function trimTree(root, is_ancestry) {
let max_depth = is_ancestry ? ancestry_depth : progeny_depth;
if (one_level_rels) max_depth = 1;
if (!max_depth && max_depth !== 0) return root
trimNode(root, 0);
return root
function trimNode(node, depth) {
if (depth === max_depth) {
if (node.children) delete node.children;
} else if (node.children) {
node.children.forEach(child => {
trimNode(child, depth+1);
});
}
}
}
function addSpouseReferences(root) {
addSpouses(root);
function addSpouses(d) {
if (d.children && d.children.length === 2) {
d.children[0]._spouse = d.children[1];
d.children[1]._spouse = d.children[0];
}
if (d.children) d.children.forEach(d0 => addSpouses(d0));
}
}
function setupFromTo(tree) {
tree.forEach(d => {
if (d.data.main) {
d.from = [];
d.to = [];
d.to_ancestry = d.parents;
} else if (d.is_ancestry) {
d.from = [d.parent];
d.to = d.parents;
} else {
if (d.added) {
d.from = [];
d.from_spouse = d.spouse;
d.to = [];
return
}
if (d.sibling) return
const p1 = d.parent;
const p2 = (d.parent.spouses || []).find(d0 => d0.data.id === d.data.rels.father || d0.data.id === d.data.rels.mother);
d.from = [p1];
if (p2) d.from.push(p2);
if (!p1.to) p1.to = [];
p1.to.push(d);
if (p2) {
if (!p2.to) p2.to = [];
p2.to.push(d);
}
}
});
}
function handleDuplicateHierarchy(root, data_stash, is_ancestry) {
if (is_ancestry) handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others);
else handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others);
}
}
function setupTid({tree}) {
const ids = [];
tree.forEach(d => {
if (ids.includes(d.data.id)) {
const duplicates = tree.filter(d0 => d0.data.id === d.data.id);
duplicates.forEach((d0, i) => {
d0.tid = `${d.data.id}--x${i+1}`;
d0.duplicate = duplicates.length;
ids.push(d.data.id);
});
} else {
d.tid = d.data.id;
ids.push(d.data.id);
}
});
}
function createStore(initial_state) {
let onUpdate;
const state = initial_state;
state.main_id_history = [];
const store = {
state,
updateTree: (props) => {
state.tree = calcTree();
if (!state.main_id) updateMainId(state.tree.main_id);
if (onUpdate) onUpdate(props);
},
updateData: data => state.data = data,
updateMainId,
getMainId: () => state.main_id,
getData: () => state.data,
getTree: () => state.tree,
setOnUpdate: (f) => onUpdate = f,
getMainDatum,
getDatum,
getTreeMainDatum,
getTreeDatum,
getLastAvailableMainDatum,
methods: {},
};
return store
function calcTree() {
return CalculateTree({
data: state.data, main_id: state.main_id,
node_separation: state.node_separation, level_separation: state.level_separation,
single_parent_empty_card: state.single_parent_empty_card,
is_horizontal: state.is_horizontal,
one_level_rels: state.one_level_rels,
sortChildrenFunction: state.sortChildrenFunction,
sortSpousesFunction: state.sortSpousesFunction,
ancestry_depth: state.ancestry_depth,
progeny_depth: state.progeny_depth,
show_siblings_of_main: state.show_siblings_of_main,
modifyTreeHierarchy: state.modifyTreeHierarchy,
private_cards_config: state.private_cards_config,
duplicate_branch_toggle: state.duplicate_branch_toggle
})
}
function getMainDatum() {
return state.data.find(d => d.id === state.main_id)
}
function getDatum(id) {
return state.data.find(d => d.id === id)
}
function getTreeMainDatum() {
if (!state.tree) return null;
return state.tree.data.find(d => d.data.id === state.main_id)
}
function getTreeDatum(id) {
if (!state.tree) return null;
return state.tree.data.find(d => d.data.id === id)
}
function updateMainId(id) {
if (id === state.main_id) return
state.main_id_history = state.main_id_history.filter(d => d !== id).slice(-10);
state.main_id_history.push(id);
state.main_id = id;
}
// if main_id is deleted, get the last available main_id
function getLastAvailableMainDatum() {
let main_id = state.main_id_history.slice(0).reverse().find(id => getDatum(id));
if (!main_id) main_id = state.data[0].id;
if (main_id !== state.main_id) updateMainId(main_id);
return getDatum(main_id)
}
}
function positionTree({t, svg, transition_time=2000}) {
const el_listener = svg.__zoomObj ? svg : svg.parentNode; // if we need listener for svg and html, we will use parent node
const zoom = el_listener.__zoomObj;
d3.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom
.call(zoom.transform, d3.zoomIdentity.scale(t.k).translate(t.x, t.y));
}
function treeFit({svg, svg_dim, tree_dim, with_transition, transition_time}) {
const t = calculateTreeFit(svg_dim, tree_dim);
positionTree({t, svg, with_transition, transition_time});
}
function calculateTreeFit(svg_dim, tree_dim) {
let k = Math.min(svg_dim.width / tree_dim.width, svg_dim.height / tree_dim.height);
if (k > 1) k = 1;
const x = tree_dim.x_off + (svg_dim.width - tree_dim.width*k)/k/2;
const y = tree_dim.y_off + (svg_dim.height - tree_dim.height*k)/k/2;
return {k,x,y}
}
function cardToMiddle({datum, svg, svg_dim, scale, transition_time}) {
const k = scale || 1, x = svg_dim.width/2-datum.x*k, y = svg_dim.height/2-datum.y,
t = {k, x: x/k, y: y/k};
positionTree({t, svg, with_transition: true, transition_time});
}
function createLinks({d, tree, is_horizontal=false}) {
const links = [];
// d.spouses is always added to non-ancestry side for main blodline nodes
// d._spouse is added to ancestry side
if (d.spouses || d._spouse) handleSpouse({d});
handleAncestrySide({d});
handleProgenySide({d});
return links;
function handleAncestrySide({d}) {
if (!d.parents) return
const p1 = d.parents[0];
const p2 = d.parents[1] || p1;
const p = {x: getMid(p1, p2, 'x'), y: getMid(p1, p2, 'y')};
links.push({
d: Link(d, p),
_d: () => {
const _d = {x: d.x, y: d.y},
_p = {x: d.x, y: d.y};
return Link(_d, _p)
},
curve: true,
id: linkId(d, p1, p2),
depth: d.depth+1,
is_ancestry: true,
source: d,
target: [p1, p2]
});
}
function handleProgenySide({d}) {
if (!d.children || d.children.length === 0) return
d.children.forEach((child, i) => {
const other_parent = otherP