import { mdiXml } from "@mdi/js"; import Icon from "@mdi/react"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import { IconButton, Tooltip } from "@mui/material"; import "family-chart"; import { jsPDF } from "jspdf"; import React from "react"; import "svg2pdf.js"; import { Couple } from "../../api/CoupleApi"; import { Member, fmtDate } from "../../api/MemberApi"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { FamilyTreeNode, getAvailableMembers, treeHeight, treeWidth, } from "../../utils/family_tree"; import { downloadBlob } from "../../utils/files_utils"; import "./family-chart.css"; export function ComplexFamilyTree(p: { tree: FamilyTreeNode; isUp: boolean; depth: number; }): React.ReactElement { const darkTheme = useDarkTheme(); console.log(f3); const applyTree = (container: HTMLDivElement) => { if (!container) return; const store = f3.createStore({ data: treeToF3Data(p.tree, p.isUp, p.depth), node_separation: 250, level_separation: 150, }); const view = f3.d3AnimationView({ store, cont: container, }); const Card = f3.elements.Card({ store, svg: view.svg, card_dim: { w: 210, h: 120, text_x: 5, text_y: 75, img_w: 60, img_h: 70, img_x: 5, img_y: 5, }, card_display: [ (d) => `${d.data.first_name || ""} ${d.data.last_name || ""} ${ d.data.dead ? "✝" : "" }`, (d) => { let birthDeath = []; if (d.data.birthday) birthDeath.push(d.data.birthday); if (d.data.deathday) birthDeath.push(d.data.deathday); let s = birthDeath.join(" -> "); 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 += `</tspan> <tspan x="0" dy="14" font-size="10">${weddingInfo.join( " - " )}`; } return s; }, ], mini_tree: true, link_break: false, }); // Patch generated card const PatchedCard: f3.F3CardBuilder = (p) => { const res = Card(p); // Patch card colors for PDF export res .querySelector(".card-male") ?.querySelector(".card-body-rect") ?.setAttribute("fill", "#add8e6"); res .querySelector(".card-female") ?.querySelector(".card-body-rect") ?.setAttribute("fill", "#ffb6c1"); return res; }; view.setCard(PatchedCard); store.setOnUpdate((props) => view.update(props || {})); store.update.tree({ initial: false, transition_time: 0 }); }; const doExport = async (onlySVG: boolean) => { const docWidth = treeWidth(p.tree) * 65; const docHeight = treeHeight(p.tree) * 60; console.info(`Tree w=${treeWidth(p.tree)} h=${treeHeight(p.tree)}`); // Clone the SVG to manipulate it const container = document.createElement("div"); container.classList.add("f3", "f3-export"); container.style.width = docWidth + "px"; 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( `<path class="link" fill="none" stroke="#fff"`, `<path class="link" fill="none" stroke="#000"` ); dstSVG = dstSVG.replaceAll( `class="text-overflow-mask"`, `class="text-overflow-mask" fill="transparent"` ); dstSVG = dstSVG.replaceAll(`>UNKNOWN<`, `fill="#000">INCONNU<`); dstSVG = dstSVG.replaceAll( `class="card-outline`, `fill="transparent" class="card-outline` ); dstSVG = dstSVG.replaceAll("✝", " "); // Download in SVG format if (onlySVG) { // Fix background color (first rect background) dstSVG = dstSVG.replace( `fill="transparent"></rect>`, `fill="white"></rect>` ); const blob = new Blob([`<svg>${dstSVG}</svg>`], { type: "image/svg+xml", }); downloadBlob(blob, "ArbreGenealogique.svg"); return; } // Download in PDF format //navigator.clipboard.writeText(dstSVG); target.innerHTML = dstSVG; const doc = new jsPDF({ orientation: "l", format: [docHeight, docWidth], }); await doc.svg(target, { height: docHeight, width: docWidth, }); container.remove(); // Save the created pdf doc.save("ArbreGenealogique.pdf"); }; const exportPDF = () => doExport(false); const exportSVG = () => doExport(true); return ( <div> <div style={{ textAlign: "right" }}> <Tooltip title="Exporter le graphique au format PDF"> <IconButton onClick={exportPDF}> <PictureAsPdfIcon /> </IconButton> </Tooltip> <Tooltip title="Exporter le graphique au format SVG"> <IconButton onClick={exportSVG}> <Icon path={mdiXml} size={1} /> </IconButton> </Tooltip> </div> <div style={{ width: "100%" }} className={`f3 ${darkTheme.enabled ? "f3-dark" : "f3-light"}`} id="FamilyChart" ref={applyTree} ></div> </div> ); } function treeToF3Data( node: FamilyTreeNode, isUp: boolean, depth: number ): f3.f3Data[] { const availableMembers = getAvailableMembers(node, depth); const list: f3.f3Data[] = []; if (isUp) treeToF3DataUpRecurse(node, list, availableMembers); else treeToF3DataDownRecurse(node, list, availableMembers); return list; } 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: f3.f3Data[], availableMembers: Set<number>, child?: number, spouses?: number[] ) { if (!availableMembers.has(node.member.id)) return; 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: f3.f3Data[], availableMembers: Set<number> ) { if (!availableMembers.has(node.member.id)) return; // Get all members ids let children = node?.down?.map((c) => c.member.id) ?? []; node.couples?.map((c) => c.down.forEach((m) => children.push(m.member.id))); children = children.filter((c) => availableMembers.has(c)); 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 ?.filter((s) => availableMembers.has(s.member.id)) .map((c) => c.member.id.toString()), children: children.map((c) => c.toString()), }, }); node?.down?.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); if (node.couples) { for (const c of node.couples) { if (!availableMembers.has(c.member.id)) continue; 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 .filter((c) => availableMembers.has(c.member.id)) .map((c) => c.member.id.toString()), }, }); c.down.forEach((e) => treeToF3DataDownRecurse(e, array, availableMembers) ); } } }