UNPKG

famtree-charted

Version:
1,468 lines (1,254 loc) 105 kB
// undefined v0.5.2 Copyright 2025 donatso edited by bagusandre (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.f3 = factory()); }(this, (function () { 'use strict'; // import * as _d3 from 'd3'; // comment this line to run locally with http-server var d3 = typeof window === "object" && !!window.d3 ? window.d3 : _d3; function sortChildrenWithSpouses(data) { data.forEach(datum => { if (!datum.rels.children) return const spouses = datum.rels.spouses || []; datum.rels.children.sort((a, b) => { const a_d = data.find(d => d.id === a), b_d = data.find(d => d.id === b), a_p2 = otherParent(a_d, datum, data) || {}, b_p2 = otherParent(b_d, datum, data) || {}, a_i = spouses.indexOf(a_p2.id), b_i = spouses.indexOf(b_p2.id); if (datum.data.gender === "M") return a_i - b_i else return b_i - a_i }); }); } 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 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) { 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 createForm({datum, store, fields, postSubmit, addRelative, deletePerson, onCancel, editFirst}) { const form_creator = { fields: [], onSubmit: submitFormChanges, }; 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.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 (form_creator.onDelete) form_creator.can_delete = checkIfRelativesConnectedWithoutPerson(datum, store.getData()); if (editFirst) form_creator.editable = true; form_creator.gender_field = { id: 'gender', type: 'switch', label: 'Gender', initial_value: datum.data.gender, options: [{value: 'M', label: 'Male'}, {value: 'F', label: 'Female'}] }; fields.forEach(d => { const field = { id: d.id, type: d.type, label: d.label, initial_value: datum.data[d.id], }; form_creator.fields.push(field); }); return form_creator function submitFormChanges(e) { e.preventDefault(); const form_data = new FormData(e.target); form_data.forEach((v, k) => datum.data[k] = v); if (datum.to_add) delete datum.to_add; postSubmit(); } function deletePersonWithPostSubmit() { deletePerson(); postSubmit({delete: true}); } } 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)) return {success: false, error: 'checkIfRelativesConnectedWithoutPerson'} 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); } } }); 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 cleanupDataJson(data_json) { let data_no_to_add = JSON.parse(data_json); data_no_to_add.forEach(d => d.to_add ? removeToAdd(d, data_no_to_add) : d); data_no_to_add.forEach(d => delete d.main); data_no_to_add.forEach(d => delete d.hide_rels); return JSON.stringify(data_no_to_add, null, 2) } 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 handleNewRel({datum, new_rel_datum, data_stash}) { const rel_type = new_rel_datum._new_rel_data.rel_type; delete new_rel_datum._new_rel_data; new_rel_datum = JSON.parse(JSON.stringify(new_rel_datum)); // to keep same datum state in current add relative tree if (rel_type === "son" || rel_type === "daughter") { let mother = data_stash.find(d => d.id === new_rel_datum.rels.mother); let father = data_stash.find(d => d.id === new_rel_datum.rels.father); new_rel_datum.rels = {}; if (father) { if (!father.rels.children) father.rels.children = []; father.rels.children.push(new_rel_datum.id); new_rel_datum.rels.father = father.id; } if (mother) { if (!mother.rels.children) mother.rels.children = []; mother.rels.children.push(new_rel_datum.id); new_rel_datum.rels.mother = mother.id; } } else if (rel_type === "spouse") { if (!datum.rels.spouses) datum.rels.spouses = []; if (!datum.rels.spouses.includes(new_rel_datum.id)) datum.rels.spouses.push(new_rel_datum.id); // if rel is added in same same add relative tree then we need to clean up duplicate parent new_rel_datum.rels.children = new_rel_datum.rels.children.filter(child_id => { const child = data_stash.find(d => d.id === child_id); if (!child) return false if (child.rels.mother !== datum.id) { if (data_stash.find(d => d.id === child.rels.mother)) data_stash.splice(data_stash.findIndex(d => d.id === child.rels.mother), 1); child.rels.mother = new_rel_datum.id; } if (child.rels.father !== datum.id) { if (data_stash.find(d => d.id === child.rels.father)) data_stash.splice(data_stash.findIndex(d => d.id === child.rels.father), 1); child.rels.father = new_rel_datum.id; } return true }); new_rel_datum.rels = { spouses: [datum.id], children: new_rel_datum.rels.children }; } else if (rel_type === "father") { datum.rels.father = new_rel_datum.id; new_rel_datum.rels = { children: [datum.id], }; if (datum.rels.mother) { new_rel_datum.rels.spouses = [datum.rels.mother]; const mother = data_stash.find(d => d.id === datum.rels.mother); if (!mother.rels.spouses) mother.rels.spouses = []; mother.rels.spouses.push(new_rel_datum.id); } } else if (rel_type === "mother") { datum.rels.mother = new_rel_datum.id; new_rel_datum.rels = { children: [datum.id], }; if (datum.rels.father) { new_rel_datum.rels.spouses = [datum.rels.father]; const father = data_stash.find(d => d.id === datum.rels.father); if (!father.rels.spouses) father.rels.spouses = []; father.rels.spouses.push(new_rel_datum.id); } } data_stash.push(new_rel_datum); } 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 zoom = svg.__zoomObj; d3.select(svg).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 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 CalculateTree({data, main_id=null, node_separation=250, level_separation=150, single_parent_empty_card=true, is_horizontal=false}) { 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; sortChildrenWithSpouses(data_stash); 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}); setupProgenyParentsPos({tree}); nodePositioning({tree}); tree.forEach(d => d.all_rels_displayed = isAllRelativeDisplayed(d, 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, d3_tree = d3.tree().nodeSize([node_separation, level_separation]).separation(separation), root = d3.hierarchy(datum, hierarchyGetter); d3_tree(root); return root.descendants() function separation(a, b) { let offset = 1; if (!is_ancestry) { if (!sameParent(a, b)) offset+=.25; 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) { return [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id)) } 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){ 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 const m = findDatum(d.data.rels.mother); const f = findDatum(d.data.rels.father); if (m && f) { if (!m.added && !f.added) console.error('no added spouse', m, f); const added_spouse = m.added ? m : f; setupParentPos(d, added_spouse); } else if (m || f) { const parent = m || f; 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 findDatum(id) { if (!id) return null return tree.find(d => d.data.id === id) } } 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 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 (!spouse) { spouse = createToAddSpouse(d); d.rels.spouses.push(spouse.id); } spouse.rels.children.push(child.id); child.rels[!is_father ? 'father' : 'mother'] = spouse.id; }); } } to_add_spouses.forEach(d => data.push(d)); return data 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); return spouse } } } 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 }) } 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.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 = []; if (d.data.rels.spouses && d.data.rels.spouses.length > 0) 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 = otherParent(child, d, tree) || d; const sx = other_parent.sx; const parent_pos = !is_horizontal ? {x: sx, y: d.y} : {x: d.x, y: sx}; links.push({ d: Link(child, parent_pos), _d: () => Link(parent_pos, {x: _or(parent_pos, 'x'), y: _or(parent_pos, 'y')}), curve: true, id: linkId(child, d, other_parent), depth: d.depth+1, is_ancestry: false, source: [d, other_parent], target: child }); }); } function handleSpouse({d}) { d.data.rels.spouses.forEach(sp_id => { const spouse = getRel(d, tree, d0 => d0.data.id === sp_id); if (!spouse || d.spouse) return links.push({ d: [[d.x, d.y], [spouse.x, spouse.y]], _d: () => [ d.is_ancestry ? [_or(d, 'x')-.0001, _or(d, 'y')] : [d.x, d.y], // add -.0001 to line to have some length if d.x === spouse.x d.is_ancestry ? [_or(spouse, 'x'), _or(spouse, 'y')] : [d.x-.0001, d.y] ], curve: false, id: linkId(d, spouse), depth: d.depth, spouse: true, is_ancestry: spouse.is_ancestry, source: d, target: spouse }); }); } /// function getMid(d1, d2, side, is_) { if (is_) return _or(d1, side) - (_or(d1, side) - _or(d2, side))/2 else return d1[side] - (d1[side] - d2[side])/2 } function _or(d, k) { return d.hasOwnProperty('_'+k) ? d['_'+k] : d[k] } function Link(d, p) { return is_horizontal ? LinkHorizontal(d, p) : LinkVertical(d, p) } function LinkVertical(d, p) { const hy = (d.y + (p.y - d.y) / 2); return [ [d.x, d.y], [d.x, hy], [d.x, hy], [p.x, hy], [p.x, hy], [p.x, p.y], ] } function LinkHorizontal(d, p) { const hx = (d.x + (p.x - d.x) / 2); return [ [d.x, d.y], [hx, d.y], [hx, d.y], [hx, p.y], [hx, p.y], [p.x, p.y], ] } function linkId(...args) { return args.map(d => d.data.id).sort().join(", ") // make unique id } function otherParent(child, p1, data) { const condition = d0 => (d0.data.id !== p1.data.id) && ((d0.data.id === child.data.rels.mother) || (d0.data.id === child.data.rels.father)); return getRel(p1, data, condition) } // if there is overlapping of personas in different branches of same family tree, return the closest one function getRel(d, data, condition) { const rels = data.filter(condition); const dist_xy = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); if (rels.length > 1) return rels.sort((d0, d1) => dist_xy(d0, d) - dist_xy(d1, d))[0] else return rels[0] } } function pathToMain(cards, links, datum, main_datum) { const is_ancestry = datum.is_ancestry; const links_data = links.data(); let links_node_to_main = []; let cards_node_to_main = []; if (is_ancestry) { const links_to_main = []; let parent = datum; let itteration1 = 0; while (parent !== main_datum.data && itteration1 < 100) { itteration1++; // to prevent infinite loop const spouse_link = links_data.find(d => d.spouse === true && (d.source === parent || d.target === parent)); if (spouse_link) { const child_link = links_data.find(d => Array.isArray(d.target) && d.target.includes(spouse_link.source) && d.target.includes(spouse_link.target)); if (!child_link) break links_to_main.push(spouse_link); links_to_main.push(child_link); parent = child_link.source; } else { // single parent const child_link = links_data.find(d => Array.isArray(d.target) && d.target.includes(parent)); if (!child_link) break links_to_main.push(child_link); parent = child_link.source; } } links.each(function(d) { if (links_to_main.includes(d)) { links_node_to_main.push({link: d, node: this}); } }); const cards_to_main = getCardsToMain(datum, links_to_main); cards.each(function(d) { if (cards_to_main.includes(d)) { cards_node_to_main.push({card: d, node: this}); } }); } else if (datum.spouse && datum.spouse.data === main_datum.data) { links.each(function(d) { if (d.target === datum) links_node_to_main.push({link: d, node: this}); }); const cards_to_main = [main_datum, datum]; cards.each(function(d) { if (cards_to_main.includes(d)) { cards_node_to_main.push({card: d, node: this}); } }); } else { let links_to_main = []; let child = datum; let itteration1 = 0; while (child !== main_datum.data && itteration1 < 100) { itteration1++; // to prevent infinite loop const child_link = links_data.find(d => d.target === child && Array.isArray(d.source)); if (child_link) { const spouse_link = links_data.find(d => d.spouse === true && sameArray([d.source, d.target], child_link.source)); links_to_main.push(child_link); links_to_main.push(spouse_link); if (spouse_link) child = spouse_link.source; else child = child_link.source[0]; } else { const spouse_link = links_data.find(d => d.target === child && !Array.isArray(d.source)); // spouse link if (!spouse_link) break links_to_main.push(spouse_link); child = spouse_link.source; } } links.each(function(d) { if (links_to_main.includes(d)) { links_node_to_main.push({link: d, node: this}); } }); const cards_to_main = getCardsToMain(main_datum, links_to_main); cards.each(function(d) { if (cards_to_main.includes(d)) { cards_node_to_main.push({card: d, node: this}); } }); } return [cards_node_to_main, links_node_to_main] function sameArray(arr1, arr2) { return arr1.every(d1 => arr2.some(d2 => d1 === d2)) } function getCardsToMain(first_parent, links_to_main) { const all_cards = links_to_main.filter(d => d).reduce((acc, d) => { if (Array.isArray(d.target)) acc.push(...d.target); else acc.push(d.target); if (Array.isArray(d.source)) acc.push(...d.source); else acc.push(d.source); return acc }, []); const cards_to_main = [main_datum, datum]; getChildren(first_parent); return cards_to_main function getChildren(d) { if (d.data.rels.children) { d.data.rels.children.forEach(child_id => { const child = all_cards.find(d0 => d0.data.id === child_id); if (child) { cards_to_main.push(child); getChildren(child); } }); } } } } function createPath(d, is_) { const line = d3.line().curve(d3.curveMonotoneY), lineCurve = d3.line().curve(d3.curveBasis), path_data = is_ ? d._d() : d.d; if (!d.curve) return line(path_data) else if (d.curve === true) return lineCurve(path_data) } function updateLinks(svg, tree, props={}) { const links_data_dct = tree.data.reduce((acc, d) => { createLinks({d, tree:tree.data, is_horizontal: tree.is_horizontal}).forEach(l => acc[l.id] = l); return acc }, {}); const links_data = Object.values(links_data_dct); const link = d3.select(svg).select(".links_view").selectAll("path.link").data(links_data, d => d.id); const link_exit = link.exit(); const link_enter = link.enter().append("path").attr("class", "link"); const link_update = link_enter.merge(link); link_exit.each(linkExit); link_enter.each(linkEnter); link_update.each(linkUpdate); function linkEnter(d) { d3.select(this).attr("fill", "none").attr("stroke", "#fff").attr("stroke-width", 1).style("opacity", 0) .attr("d", createPath(d, true)); } function linkUpdate(d) { const path = d3.select(this); const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; path.transition('path').duration(props.transition_time).delay(delay).attr("d", createPath(d)).style("opacity", 1); } function linkExit(d) { const path = d3.select(this); path.transition('op').duration(800).style("opacity", 0); path.transition('path').duration(props.transition_time).attr("d", createPath(d, true)) .on("end", () => path.remove()); } } function updateCards(svg, tree, Card, props={}) { const card = d3.select(svg).select(".cards_view").selectAll("g.card_cont").data(tree.data, d => d.data.id), card_exit = card.exit(), card_enter = card.enter().append("g").attr("class", "card_cont"), card_update = card_enter.merge(card); card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); card_exit.each(cardExit); card.each(cardUpdateNoEnter); card_enter.each(cardEnter); card_update.each(cardUpdate); function cardEnter(d) { d3.select(this) .attr("transform", `translate(${d._x}, ${d._y})`) .style("opacity", 0); Card.call(this, d); } function cardUpdateNoEnter(d) {} function cardUpdate(d) { Card.call(this, d); const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; d3.select(this).transition().duration(props.transition_time).delay(delay).attr("transform", `translate(${d.x}, ${d.y})`).style("opacity", 1); } function cardExit(d) { const g = d3.select(this); g.transition().duration(props.transition_time).style("opacity", 0).attr("transform", `translate(${d._x}, ${d._y})`) .on("end", () => g.remove()); } } function updateCardsHtml(div, tree, Card, props={}) { const card = d3.select(div).select(".cards_view").selectAll("div.card_cont").data(tree.data, d => d.data.id), card_exit = card.exit(), card_enter = card.enter().append("div").attr("class", "card_cont").style('pointer-events', 'none'), card_update = card_enter.merge(card); card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); card_exit.each(cardExit); card.each(cardUpdateNoEnter); card_enter.each(cardEnter); card_update.each(cardUpdate); function cardEnter(d) { d3.select(this) .style('position', 'absolute') .style('top', '0').style('left', '0') .style("transform", `translate(${d._x}px, ${d._y}px)`) .style("opacity", 0); Card.call(this, d); } function cardUpdateNoEnter(d) {} function cardUpdate(d) { Card.call(this, d); const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; d3.select(this).transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); } function cardExit(d) { const g = d3.select(this); g.transition().duration(props.transition_time).style("opacity", 0).style("transform", `translate(${d._x}px, ${d._y}px)`) .on("end", () => g.remove()); } } function assignUniqueIdToTreeData(div, tree_data) { const card = d3.select(div).selectAll("div.card_cont_2fake").data(tree_data, d => d.data.id); // how this doesn't break if there is multiple cards with the same id? const card_exit = card.exit(); const card_enter = card.enter().append("div").attr("class", "card_cont_2fake").style('display', 'none').attr("data-id", () => Math.random()); const card_update = card_enter.merge(card); card_exit.each(cardExit); card_enter.each(cardEnter); card_update.each(cardUpdate); function cardEnter(d) { d.unique_id = d3.select(this).attr("data-id"); } function cardUpdate(d) { d.unique_id = d3.select(this).attr("data-id"); } function cardExit(d) { d.unique_id = d3.select(this).attr("data-id"); d3.select(this).remove(); } } function setupHtmlSvg(getHtmlSvg) { d3.select(getHtmlSvg()).append("div").attr("class", "cards_view_fake").style('display', 'none'); // important for handling data } function getCardsViewFake(getHtmlSvg) { return d3.select(getHtmlSvg()).select("div.cards_view_fake").node() } function onZoomSetup(getSvgView, getHtmlView) { return function onZoom(e) { const t = e.transform; d3.select(getSvgView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); d3.select(getHtmlView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); } } function setupReactiveTreeData(getHtmlSvg) { let tree_data = []; return function getReactiveTreeData(new_tree_data) { const tree_data_exit = getTreeDataExit(new_tree_data, tree_data); tree_data = [...new_tree_data, ...tree_data_exit]; assignUniqueIdToTreeData(getCardsViewFake(getHtmlSvg), tree_data); return tree_data } } function createHtmlSvg(cont) { const f3Canvas = d3.select(cont).select('#f3Canvas'); const cardHtml = f3Canvas.append('div').attr('id', 'htmlSvg') .attr('style', 'position: absolute; width: 100%; height: 100%; z-index: 2; top: 0; left: 0'); cardHtml.append('div').attr('class', 'cards_view').style('transform-origin', '0 0'); setupHtmlSvg(() => cardHtml.node()); return cardHtml.node() } function getTreeDataExit(new_tree_data, old_tree_data) { if (old_tree_data.length > 0) { return old_tree_data.filter(d => !new_tree_data.find(t => t.data.id === d.data.id)) } else { return [] } } function getUniqueId(d) { return d.unique_id } var htmlHandlers = /*#__PURE__*/Object.freeze({ __proto__: null, assignUniqueIdToTreeData: assignUniqueIdToTreeData, setupHtmlSvg: setupHtmlSvg, getCardsViewFake: getCardsViewFake, onZoomSetup: onZoomSetup, setupReactiveTreeData: setupReactiveTreeData, createHtmlSvg: createHtmlSvg, getUniqueId: getUniqueId }); function updateCardsComponent(div, tree, Card, props={}) { const card = d3.select(getCardsViewFake(() => div)).selectAll("div.card_cont_fake").data(tree.data, d => d.data.id), card_exit = card.exit(), card_enter = card.enter().append("div").attr("class", "card_cont_fake").style('display', 'none'), card_update = card_enter.merge(card); card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); card_exit.each(cardExit); card.each(cardUpdateNoEnter); card_enter.each(cardEnter); card_update.each(cardUpdate); function cardEnter(d) { const card_element = d3.select(Card(d)); card_element .style('position', 'absolute') .style('top', '0').style('left', '0').style("opacity", 0) .style("transform", `translate(${d._x}px, ${d._y}px)`); } function cardUpdateNoEnter(d) {} function cardUpdate(d) { const card_element = d3.select(Card(d)); const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; card_element.transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); } function cardExit(d) { const card_element = d3.select(Card(d)); const g = d3.select(this); card_element.transition().duration(props.transition_time).style("opacity", 0).style("transform", `translate(${d._x}px, ${d._y}px)`) .on("end", () => g.remove()); // remove the card_cont_fake } } function view(tree, svg, Card, props={}) { props.initial = props.hasOwnProperty('initial') ? props.initial : !d3.select(svg.parentNode).select('.card_cont').node(); props.transition_time = props.hasOwnProperty('transition_time') ? props.transition_time : 2000; if (props.cardComponent) updateCardsComponent(props.cardComponent, tree, Card, props); else if (props.cardHtml) updateCardsHtml(props.cardHtml, tree, Card, props); else updateCards(svg, tree, Card, props); updateLinks(svg, tree, props); const tree_position = props.tree_position || 'fit'; if (props.initial) treeFit({svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: 0}); else if (tree_position === 'fit') treeFit({svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: props.transition_time}); else if (tree_position === 'main_to_middle') cardToMiddle({datum: tree.data[0], svg, svg_dim: svg.getBoundingClientRect(), scale: props.scale, transition_time: props.transition_time}); else ; return true } function calculateDelay(tree, d, transition_time) { const delay_level = transition_time*.4, ancestry_levels = Math.max(...tree.data.map(d=>d.is_ancestry ? d.depth : 0)); let delay = d.depth*delay_level; if ((d.depth !== 0 || !!d.spouse) && !d.is_ancestry) { delay+=(ancestry_levels)*delay_level; // after ancestry if (d.spouse) delay+=delay_level; // spouse after bloodline delay+=(d.depth)*delay_level; // double the delay for each level because of additional spouse delay } return delay } function createSvg(cont, props={}) { const svg_dim = cont.getBoundingClientRect(); const svg_html = (` <svg class="main_svg"> <rect width="${svg_dim.width}" height="${svg_dim.height}" fill="transparent" /> <g class="view"> <g class="links_view"></g> <g class="cards_view"></g> </g> <g style="transform: translate(100%, 100%)"> <g class="fit_screen_icon cursor-pointer" style="transform: translate(-50px, -50px); display: none"> <rect width="27" height="27" stroke-dasharray="${27/2}" stroke-dashoffset="${27/4}" style="stroke:#fff;stroke-width:4px;fill:transparent;"/> <circle r="5" cx="${27/2}" cy="${27/2}" style="fill:#fff" /> </g> </g> </svg> `); const f3Canvas = getOrCreateF3Canvas(cont); const temp_div = d3.create('div').node(); temp_div.innerHTML = svg_html; const svg = temp_div.querySelector('svg'); f3Canvas.appendChild(svg); cont.appendChild(f3Canvas); setupZoom(f3Canvas, props); return svg function getOrCreateF3Canvas(cont) { let f3Canvas = cont.querySelector('#f3Canvas'); if (!f3Canvas) { f3Canvas = d3.create('div').attr('id', 'f3Canvas').attr('style', 'position: relative; overflow: hidden; width: 100%; height: 100%;').node(); } return f3Canvas } } function setupZoom(el, props={}) { if (el.__zoom) return const view = el.querySelector('.view'), zoom = d3.zoom().on("zoom", (props.onZoom || zoomed)); d3.select(el).call(zoom); el.__zoomObj = zoom; if (props.zoom_polite) zoom.filter(zoomFilter); function zoomed(e) { d3.select(view).attr("transform", e.transform); } function zoomFilter(e) { if (e.type === "wheel" && !e.ctrlKey) return false else if (e.touches && e.touches.length < 2) return false else return true } } function cardChangeMain(store, {d}) { toggleAllRels(store.getTree().data, false); store.updateMainId(d.data.id); store.updateTree({tree_position: store.state.tree_fit_on_change}); return true } function cardEdit(store, {d, cardEditForm}) { const datum = d.data, postSubmit = (props) => { if (datum.to_add) moveToAddToAdded(datum, store.getData()); if (props && props.delete) { if (datum.main) store.updateMainId(null); deletePerson(datum, store.getData()); } store.updateTree(); }; cardEditForm({datum, postSubmit, store}); } function cardShowHideRels(store, {d}) { d.data.hide_rels = !d.data.hide_rels; toggleRels(d, d.data.hide_rels); store.updateTree({tree_position: store.state.tree_fit_on_change}); } function userIcon() { return (` <g data-icon="user"> ${bgCircle()} <path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" /> </g> `) } function userEditIcon() { return (` <g data-icon="user-edit"> ${bgCircle()} <path d="M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91, 12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4, 18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z" /> </g> `) } function userPlusIcon() { return (` <g data-icon="user-plus"> ${bgCircle()} <path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M6,10V7H4V10H1V12H4V15H6V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" /> </g> `) } function userPlusCloseIcon() { return (` <g data-icon="user-plus-close"> ${bgCircle()} <path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M6,10V7H4V10H1V12H4V15H6V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" /> <line x1="3" y1="3" x2="24" y2="24" stroke="currentColor" stroke-width="2" /> </g> `) } function plusIcon() { return (` <g data-icon="plus"> ${bgCircle()} <path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /> </g> `) } function pencilIcon() { return (` <g data-icon="pencil"> ${bgCircle()} <path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" /> </g> `) } function pencilOffIcon() { return (` <g data-icon="pencil-off"> ${bgCircle()} <path d="M18.66,2C18.4,2 18.16,2.09 17.97,2.28L16.13,4.13L19.88,7.88L21.72,6.03C22.11,5.64 22.11,5 21.72,4.63L19.38,2.28C19.18,2.09 18.91,2 18.66,2M3.28,4L2,5.28L8.5,11.75L4,16.25V20H7.75L12.25,15.5L18.72,22L20,20.72L13.5,14.25L9.75,10.5L3.28,4M15.06,5.19L11.03,9.22L14.78,12.97L18.81,8.94L15.06,5.19Z" /> </g> `) } function trashIcon() { return (` <g data-icon="trash"> ${bgCircle()} <path d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" /> </g> `) } function historyBackIcon() { return (` <g data-icon="history-back"> ${bgCircle()} <path d="M20 13.5C20 17.09 17.09 20 13.5 20H6V18H13.5C16 18 18 16 18 13.5S16 9 13.5 9H7.83L10.91 12.09L9.5 13.5L4 8L9.5 2.5L10.92 3.91L7.83 7H13.5C17.09 7 20 9.91 20 13.5Z" /> </g> `) } function historyForwardIcon() { return (` <g data-icon="history-forward"> ${bgCircle()} <path d="M10.5 18H18V20H10.5C6.91 20 4 17.09 4 13.5S6.91 7 10.5 7H16.17L13.08 3.91L14.5 2.5L20 8L14.5 13.5L13.09 12.09L16.17 9H10.5C8 9 6 11 6 13.5S8 18 10.5 18Z" /> </g> `) } function personIcon() { return (` <g data-icon="person"> <path d="M256 288c79.5 0 144-64.5 144-144S335.5 0 256 0 112 64.5 112 144s64.5 144 144 144zm128 32h-55.1c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16H128C57.3 320 0 377.3 0 448v16c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48v-16c0-70.7-57.3-128-128-128z" /> </g> `) } function miniTreeIcon() { return (` <g transform="translate(31,25)" data-icon="mini-tree"> <rect x="-31" y="-25" width="72" height="15" fill="rgba(0,0,0,0)"></rect> <g> <rect x="-31" y="-25" width="72" height="15" fill="rgba(0,0,0,0)"></rect> <line y2="-17.5" stroke="#fff" /> <line x1="-20" x2="20" y1="-17.5" y2="-17.5" stroke="#fff" /> <rect x="-31" y="-25" width="25" height="15" rx="5" ry="5" class="card-male" /> <rect x="6" y="-25" width="25" height="15" rx="5" ry="5" class="card-female" /> </g> </g> `) } function userSvgIcon() { return svgWrapper(userIcon()) } function userEditSvgIcon() { return svgWrapper(userEditIcon()) } function userPlusSvgIcon() { return svgWrapper(userPlusIcon()) } function us