UNPKG

gedcom-d3

Version:

A GEDCOM parser, which translates GEDCOM (.ged) files into D3 capable JSON. This package is specifically designed to prepare data for use in https://github.com/oh-kay-blanket/blood-lines

627 lines (563 loc) 16.1 kB
const d3ize = (tree) => { const notes = tree.filter(hasTag("NOTE")); let surnameList = []; const peopleNodes = tree .filter(hasTag("INDI")) .map((p) => toNode(p, notes, surnameList)); const families = tree.filter(hasTag("FAM")); const links = families.reduce((memo, family) => { return memo.concat(familyLinks(family, peopleNodes)); }, []); assignFy(peopleNodes, links); return { nodes: peopleNodes, links: links, families: families, surnameList: surnameList, }; }; // Tag search function const hasTag = (val) => { return (node) => { return node.tag === val; }; }; // Data search function const hasData = (val) => { return (node) => { return node.data === val; }; }; // ID search function const hasID = (val) => { return (node) => { return node.id === val; }; }; const assignFy = (peopleNodes, links) => { // YOB known let yesyob = peopleNodes.filter((p) => { return p.yob !== "?" && !isNaN(+p.yob); }); yesyob.forEach((p) => (p.fy = +p.yob)); // YOB unknown let noyob = peopleNodes.filter((p) => { return p.yob === "?"; }); let count = 10; // Cycle through list, adding fy until all complete while (noyob.length > 0 && count > 0) { let tempnoyob = noyob.slice(); tempnoyob.forEach((p, index) => { // Build array of family let tpFamily = []; links.forEach((link) => { if (link.source == p.id) { tpFamily.push({ pRole: "source", pType: link.sourceType, other: link.target, oType: link.targetType, }); } else if (link.target == p.id) { tpFamily.push({ pRole: "target", pType: link.targetType, other: link.source, oType: link.sourceType, }); } }); // Check family for YOB tpFamily.forEach((member) => { // USE SOME() INSTEAD OF FOREACH!!! peopleNodes.forEach((person) => { // USE SOME() INSTEAD OF FOREACH!!! if (person.id == member.other && person.fy !== undefined) { // Person is source if (member.pRole === "source") { // Person is husband if (member.pType === "HUSB" && member.oType === "WIFE") { p.fy = +person.fy - 3; // Person is father } else if (member.pType === "HUSB" && member.oType === "CHIL") { p.fy = +person.fy - 30; // Person is mother } else if (member.pType === "WIFE") { p.fy = +person.fy - 27; } // Person is target } else if (member.pRole === "target") { // Person is wife if (member.pType === "WIFE" && member.oType === "HUSB") { p.fy = +person.fy + 3; // Person is child of father } else if (member.pType === "CHIL" && member.oType === "HUSB") { p.fy = +person.fy + 30; // Person is child of mother } else if (member.pType === "CHIL" && member.oType === "WIFE") { p.fy = +person.fy + 27; } } } }); }); if (p.fy !== undefined) { noyob.splice(index, index + 1); } }); count -= 1; } const convertFy = (peopleNodes) => { const fyRatio = (peopleNodes) => { if (peopleNodes.length <= 50) { return 3; } else if (peopleNodes.length > 50 && peopleNodes.length <= 150) { return 4; } else if (peopleNodes.length > 150 && peopleNodes.length <= 250) { return 5; } else if (peopleNodes.length > 250) { return 6; } }; let allFy = []; peopleNodes.forEach((p) => { if (p.fy !== undefined) { p.fy = p.fy * fyRatio(peopleNodes); allFy.push(p.fy); } }); let total = 0; allFy.forEach((fy) => (total += fy)); let average = total / allFy.length; peopleNodes.forEach((p) => { if (p.fy !== undefined) { p.fy = -(p.fy - average); } }); }; convertFy(peopleNodes); }; // Get title const getTitle = (p) => { const title = p.tree.filter(hasTag("TITL")) || []; if (title.length > 0) { return title[title.length - 1].data; } }; // Get full name const getName = (p) => { let nameNode = (p.tree.filter(hasTag("NAME")) || [])[0]; if (nameNode) { return nameNode.data.replace(/\//g, ""); } else { return "?"; } }; // Get first name const getFirstName = (p) => { // Find 'NAME' tag const nameNode = (p.tree.filter(hasTag("NAME")) || [])[0]; if (nameNode) { // Find 'GIVN' tag let firstNameNode = (nameNode.tree.filter(hasTag("GIVN")) || [])[0]; if (firstNameNode) { // Remove middle name if (firstNameNode.data.search(" ") !== -1) { return firstNameNode.data.slice(0, firstNameNode.data.search(" ")); } else { return firstNameNode.data; } } else { // Fallback: extract given name from NAME value (e.g. "Harry /Potter/") const nameData = nameNode.data || ""; const slashIndex = nameData.indexOf("/"); const givenPart = (slashIndex !== -1 ? nameData.slice(0, slashIndex) : nameData).trim(); if (givenPart) { const spaceIndex = givenPart.indexOf(" "); return spaceIndex !== -1 ? givenPart.slice(0, spaceIndex) : givenPart; } return "?"; } } else { return "?"; } }; // Get surname const getSurname = (p) => { // Find 'NAME' tag const nameNode = (p.tree.filter(hasTag("NAME")) || [])[0]; if (nameNode) { // Find 'SURN' tag const surnameNode = (nameNode.tree.filter(hasTag("SURN")) || [])[0]; // If surname listed if (surnameNode) { // Remove alternate surnames if (surnameNode.data.search(",") !== -1) { return surnameNode.data.slice(0, surnameNode.data.search(",")); } else { return surnameNode.data; } // Derive surname from name } else { nameArr = nameNode.data.split(" "); // Look for forward slashes let isSlashes = nameArr.some((str) => str[0] === "/"); if (isSlashes) { return nameArr.find((str) => str[0] === "/").replace(/\//g, ""); // no slashes, use final item in array } else { nameArr[nameArr.length - 1] = nameArr[nameArr.length - 1].replace( /\//g, "", ); return nameArr.length > 1 ? nameArr[nameArr.length - 1] : "Hrm"; } } } else { return "?"; } }; // Get gender const getGender = (p) => { // Find 'SEX' tag let genderNode = (p.tree.filter(hasTag("SEX")) || [])[0]; if (genderNode) { return genderNode.data; } else { return "U"; } }; // Get date of birth const getDOB = (p) => { // Find 'BIRT' tag let dobNode = (p.tree.filter(hasTag("BIRT")) || [])[0]; if (dobNode) { // Find 'DATE' tag let dateNode = (dobNode.tree.filter(hasTag("DATE")) || [])[0]; if (dateNode) { return dateNode.data; } else { return "?"; } } else { return "?"; } }; // Extract a 4-digit year from a date string const extractYear = (dateStr) => { const match = dateStr.match(/\b(\d{4})\b/); return match ? match[1] : "?"; }; // Get year of birth const getYOB = (p) => { // Find 'BIRT' tag let dobNode = (p.tree.filter(hasTag("BIRT")) || [])[0]; if (dobNode) { // Find 'DATE' tag let dateNode = (dobNode.tree.filter(hasTag("DATE")) || [])[0]; if (dateNode) { return extractYear(dateNode.data); } else { return "?"; } } else { return "?"; } }; // Get place of birth const getPOB = (p) => { // Find 'BIRT' tag let pobNode = (p.tree.filter(hasTag("BIRT")) || [])[0]; if (pobNode) { // Find 'DATE' tag let placeNode = (pobNode.tree.filter(hasTag("PLAC")) || [])[0]; if (placeNode) { return placeNode.data; } else { return ""; } } else { return ""; } }; // Get date of death const getDOD = (p) => { // Find 'DEAT' tag let dodNode = (p.tree.filter(hasTag("DEAT")) || [])[0]; if (dodNode) { // Find 'DATE' tag let dateNode = (dodNode.tree.filter(hasTag("DATE")) || [])[0]; if (dateNode) { return dateNode.data; } else { return "?"; } } else { return "?"; } }; // Get year of death const getYOD = (p) => { let thisYear = new Date().getFullYear(); // Find 'DEAT' tag let dobNode = (p.tree.filter(hasTag("BIRT")) || [])[0]; let dodNode = (p.tree.filter(hasTag("DEAT")) || [])[0]; // If DEATH tag if (dodNode) { // Find 'DATE' tag let dateNode = (dodNode.tree.filter(hasTag("DATE")) || [])[0]; // If death date listed if (dateNode) { return extractYear(dateNode.data); } else { return "?"; } // BIRT tag, but no DEAT tag } else if (dobNode && !dodNode) { let dateNode = (dobNode.tree.filter(hasTag("DATE")) || [])[0]; // If DOB listed if (dateNode) { // If born > 100 yrs ago, call dead let birthYear = extractYear(dateNode.data); if (birthYear !== "?" && Number(birthYear) < thisYear - 100) { return "?"; } else { return "Present"; } } else { return "?"; } // no DEAT or BIRT tag } else { return "?"; } }; // Get place of birth const getPOD = (p) => { // Find 'BIRT' tag let podNode = (p.tree.filter(hasTag("DEAT")) || [])[0]; if (podNode) { // Find 'DATE' tag let placeNode = (podNode.tree.filter(hasTag("PLAC")) || [])[0]; if (placeNode) { return placeNode.data; } else { return ""; } } else { return ""; } }; // Get relatives const getFamilies = (p) => { let families = []; let pediInfo; // If child let familyNode1 = p.tree.filter(hasTag("FAMC")) || []; if (familyNode1) { for (let i = 0; i < familyNode1.length; i++) { if (familyNode1[i].tree.length > 0) { // Get pedigree info if (familyNode1[i].tree[0].tag == "PEDI") { pediInfo = { frel: familyNode1[i].tree[0].data, mrel: familyNode1[i].tree[0].data, }; } else if (familyNode1[i].tree[0].tag == "_FREL") { pediInfo = { frel: familyNode1[i].tree[0].data, mrel: familyNode1[i].tree[1].data, }; } } families.push({ id: familyNode1[i].data, pedi: pediInfo }); } } let familyNode2 = p.tree.filter(hasTag("FAMS")) || []; if (familyNode2) { for (let i = 0; i < familyNode2.length; i++) { families.push({ id: familyNode2[i].data }); } } return families; }; // Get color const getColor = (p, surnameList) => { const colorList = [ "#f6edd0", // cream "#f8a39e", // soft pink "#5bceff", // sky blue "#ffc32a", // mexican egg yolk "#8df0c2", // grass & sage "#d0a8ec", // soft royal purple "#ffa686", // coral "#adcdd8", // light blue grey "#a6b890", // olive sage "#e6f387", // olive "#eebf90", // light brown "#6e90e6", // ligt purple blue "#f8a7d0", // dry wine "#30f6d5", // sea foam "#be719d", // magenta "#e08e79", // blush "#80d152", // neon green "#9efde5", // light sea foam "#ff835a", // burnt orange "#eebd6e", // chocolate "#35f8a0", // forest "#f95162", // rouge "#d4ee5e", // lime "#e887aa", // newborn pink "#a178ea", // royal purple "#00c0de", // baby foam ]; // If color description listed in GEDCOM const dscr = (p.tree.filter(hasTag("DSCR")) || [])[0]; const foundName = surnameList.find((sName) => sName.surname === p.surname); // If surname already in list if (foundName) { foundName.count = foundName.count + 1; } else { surnameList.push({ surname: p.surname, count: 1, color: colorList[surnameList.length % colorList.length], }); } // surnameList.color = surnameList.length % colorList.length}); // If color listed assign that if (dscr) { return dscr.data; // else assign color from colorList } else { return surnameList.find((sName) => sName.surname === p.surname).color; } }; // Get person notes const getNotes = (p) => { return p.tree.filter(hasTag("NOTE")); }; // Get Bio const getBio = (p, notes) => { if (p.notes.length != 0) { let bio = ""; // Notes for person p.notes.forEach((personNote) => { // personNote.data points to NOTE object if (notes.length > 0) { notes.forEach((note) => { if (personNote.data === note.pointer) { bio += note.data; // Concat broken up note if (note.tree.length > 0) { note.tree.forEach((fragment) => (bio += fragment.data)); } } }); // personNote.data is actual note } else { bio += personNote.data; } }); return bio; } }; const getFy = (p) => { if (p.yob === "?") { return 0; } else { return +(-p.yob * 3 + 6000); } }; const toNode = (p, notes, surnameList) => { p.id = p.pointer; p.title = getTitle(p); p.name = getName(p); p.firstName = getFirstName(p); p.surname = getSurname(p); p.gender = getGender(p); p.dob = getDOB(p); p.yob = getYOB(p); p.pob = getPOB(p); p.dod = getDOD(p); p.yod = getYOD(p); p.pod = getPOD(p); p.families = getFamilies(p); p.color = getColor(p, surnameList); p.notes = getNotes(p); p.bio = getBio(p, notes); return p; }; const familyLinks = (family, peopleNodes) => { let memberLinks = []; let maritalStatus = null; let pedigree; // Filter only individual objects from family tree let memberSet = family.tree.filter(function (member) { return ( member.tag && (member.tag === "HUSB" || member.tag === "WIFE" || member.tag === "CHIL") ); }); // Filter marital status events family.tree.filter((event) => { if (event.tag === "DIV" || event.tag === "MARR") { if (maritalStatus !== "DIV") { maritalStatus = event.tag; } } }); // Iterate over each member of set to connect with other members while (memberSet.length > 1) { for (let i = 1; i < memberSet.length; i++) { // Exclude sibling relationships if (memberSet[0].tag != "CHIL") { // If marital status listed if (memberSet[0].tag == "HUSB" && memberSet[i].tag == "WIFE") { memberLinks.push({ source: memberSet[0].data, target: memberSet[i].data, sourceType: memberSet[0].tag, targetType: memberSet[i].tag, type: maritalStatus, }); } else { // Filter pedigree info function getPedigree(personID, parentType, relInfo) { // GRAMPS let person = peopleNodes.filter(hasID(personID)); let personFamily = person[0].families.filter(hasID(family.pointer)); if (parentType == "HUSB") { if (personFamily[0].pedi) { return personFamily[0].pedi.frel; } else if (relInfo.some((parent) => parent.tag === "_FREL")) { return relInfo.find((parent) => parent.tag === "_FREL").data; } } else { if (personFamily[0].pedi) { return personFamily[0].pedi.mrel; } else if (relInfo.some((parent) => parent.tag === "_MREL")) { return relInfo.find((parent) => parent.tag === "_MREL").data; } } } memberLinks.push({ source: memberSet[0].data, target: memberSet[i].data, sourceType: memberSet[0].tag, targetType: memberSet[i].tag, type: getPedigree( memberSet[i].data, memberSet[0].tag, memberSet[i].tree, ), }); } } } memberSet.splice(0, 1); } return memberLinks; }; module.exports = d3ize;