import React from "react"; import f3, { f3Data } from "family-chart"; import "./family-chart.css"; import { FamilyTreeNode } from "../../utils/family_tree"; import { Member, fmtDate } from "../../api/MemberApi"; import { Couple } from "../../api/CoupleApi"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { IconButton } from "@mui/material"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import { jsPDF } from "jspdf"; import "svg2pdf.js"; export function ComplexFamilyTree(p: { tree: FamilyTreeNode; isUp: boolean; }): React.ReactElement { const darkTheme = useDarkTheme(); const applyTree = (container: HTMLDivElement) => { if (!container) return; const store = f3.createStore({ data: treeToF3Data(p.tree, p.isUp), node_separation: 250, level_separation: 150, }), view = f3.d3AnimationView({ store, cont: container, }), Card = f3.elements.Card({ store, svg: view.svg, card_dim: { w: 230, h: 70, text_x: 75, text_y: 15, img_w: 60, img_h: 60, img_x: 5, img_y: 5, }, card_display: [ (d) => `${d.data.first_name || ""} ${d.data.last_name || ""} ${ d.data.dead ? "✝" : "" }`, (d) => { let s = `${d.data.birthday || ""} ${ d.data.deathday ? " ✝ " + d.data.deathday : "" }`; if (d.data.wedding_state || d.data.dateOfWedding) { let weddingInfo = []; if (d.data.wedding_state) weddingInfo.push(d.data.wedding_state); if (d.data.dateOfWedding) weddingInfo.push("Mariage : " + d.data.dateOfWedding); s += ` ${weddingInfo.join( " - " )}`; } return s; }, ], mini_tree: true, link_break: false, }); view.setCard(Card); store.setOnUpdate((props) => view.update(props || {})); store.update.tree({ initial: false, transition_time: 0 }); }; const exportPDF = async () => { const docHeight = treeHeight(p.tree) * 41; const doc = new jsPDF({ orientation: "l", format: [docHeight, 351], }); // Clone the SVG to manipulate it const container = document.createElement("div"); container.classList.add("f3", "f3-export"); container.style.height = docHeight + "px"; document.body.appendChild(container); applyTree(container); const target = container.children[0]; await new Promise((res) => setTimeout(() => res(null), 100)); // SVG manipulations (adaptations to export) let dstSVG = target.innerHTML.replaceAll( `UNKNOWN<`, `fill="#000">INCONNU<`); target.innerHTML = dstSVG; await doc.svg(target, { height: docHeight, width: 351, }); container.remove(); // Save the created pdf doc.save("myPDF.pdf"); }; return (
); } function treeHeight(node: FamilyTreeNode): number { let res = node.down?.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) ?? 0; node.couples?.forEach( (c) => (res = Math.max( res, c.down.reduce((prev, node) => Math.max(prev, treeHeight(node)), 0) )) ); return res + 1; } function treeToF3Data(node: FamilyTreeNode, isUp: boolean): f3Data[] { const availableMembers = new Set(); getAvailableMembers(node, availableMembers); const list: f3Data[] = []; if (isUp) treeToF3DataUpRecurse(node, list, availableMembers); else treeToF3DataDownRecurse(node, list, availableMembers); return list; } function getAvailableMembers(t: FamilyTreeNode, s: Set) { s.add(t.member.id); t.couples?.forEach((c) => { s.add(c.member.id); c.down.forEach((e) => getAvailableMembers(e, s)); }); t.down?.forEach((e) => getAvailableMembers(e, s)); } function memberData(m: Member, c?: Couple): f3.f3DataData { return { first_name: m.first_name ?? "_", last_name: m.last_name ?? "_", gender: m.sex ?? "M", avatar: m.thumbnailURL ?? undefined, dead: m.dead, birthday: m.dateOfBirth ? fmtDate(m.dateOfBirth) : undefined, deathday: m.dateOfDeath ? fmtDate(m.dateOfDeath) : undefined, wedding_state: c?.stateFr, dateOfWedding: c?.dateOfWedding ? fmtDate(c?.dateOfWedding) : undefined, }; } function treeToF3DataUpRecurse( node: FamilyTreeNode, array: f3Data[], availableMembers: Set, child?: number, spouses?: number[] ) { array.push({ data: memberData(node.member), id: node.member.id.toString(), rels: { father: node.member.father && availableMembers.has(node.member.father) ? node.member.father.toString() : undefined, mother: node.member.mother && availableMembers.has(node.member.mother) ? node.member.mother.toString() : undefined, spouses: spouses ?.filter((c) => c !== node.member.id) .map((c) => c.toString()), children: child ? [child.toString()] : undefined, }, }); const parentSpouses = node.down?.map((c) => c.member.id); node.down?.forEach((d) => treeToF3DataUpRecurse( d, array, availableMembers, node.member.id, parentSpouses ) ); } function treeToF3DataDownRecurse( node: FamilyTreeNode, array: f3Data[], availableMembers: Set ) { // Get all members ids const children = node?.down?.map((c) => c.member.id.toString()) ?? []; node.couples?.map((c) => c.down.forEach((m) => children.push(m.member.id.toString())) ); array.push({ data: memberData(node.member), id: node.member.id.toString(), rels: { father: node.member.father && availableMembers.has(node.member.father) ? node.member.father.toString() : undefined, mother: node.member.mother && availableMembers.has(node.member.mother) ? node.member.mother.toString() : undefined, spouses: node.couples?.map((c) => c.member.id.toString()), children: children, }, }); node?.down?.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); if (node.couples) { for (const c of node.couples) { array.push({ data: memberData(c.member, c.couple), id: c.member.id.toString(), rels: { father: c.member.father && availableMembers.has(c.member.father) ? c.member.father.toString() : undefined, mother: c.member.mother && availableMembers.has(c.member.mother) ? c.member.mother.toString() : undefined, spouses: [node.member.id.toString()], children: c.down.map((c) => c.member.id.toString()), }, }); c.down.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); } } }