famtree-charted
Version:
family tree creator and viewer
1,468 lines (1,254 loc) • 105 kB
JavaScript
// 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