UNPKG

family-chart

Version:
1,500 lines (1,298 loc) 183 kB
// 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