UNPKG

kingraph

Version:

Plots family trees using JavaScript and Graphviz

384 lines (338 loc) 11.1 kB
const join = require('./join'); const slugify = require('./slugify'); const normalize = require('./normalize'); const applyStyle = require('./apply_style'); const idGenerator = require('./id_generator'); const COLORS = require('./defaults/colors'); const getId = idGenerator(); const LINE = Array(76).join('#'); const LINE2 = '# ' + Array(74).join('-'); function render(data) { data = normalize(data); return join( [ 'digraph FamilyGraph {', { indent: [ applyStyle(data, [':bgcolor']), 'edge [', { indent: applyStyle(data, [':edge']) }, ']', '', 'node [', { indent: applyStyle(data, [':node']) }, ']', '', applyStyle(data, [':digraph']), renderHouse(data, data, []), ], }, '}', ], { indent: ' ' } ); } function renderHouse(data, house, path) { const people = house.people || {}; const families = house.families || []; const houses = house.houses || {}; const peopleMeat = [] Object.keys(people).forEach((key, idx)=>{ peopleMeat.push(renderPerson(data, house, people[key] || {}, path.concat([key]))) }) const meat = [ // People and families families.map((home, idx) =>renderFamily(data, house, home || {}, path.concat([idx])) ), peopleMeat ]; if (path.length === 0) { return meat; } else { const name = house.name || path[path.length - 1]; return [ '', LINE, `# House ${path}`, LINE, '', `subgraph cluster_${slugify(path)} {`, { indent: [ `label=<<b>${name}</b>>`, applyStyle(data, [':house', `:house-${path.length}`]), '', meat, ], }, '}', ]; } } function renderPerson(data, house, person, path) { let id = path[path.length - 1]; let label; let href = person.links && person.links[0]; let lifespan; if ( person.name || person.fullname || person.gender || person.born || person.died || person.picture ) { let sex = ''; if (person.sex) { if (person.sex.toLowerCase() === 'f') { sex = ' ♀'; } else if (person.sex.toLowerCase() === 'm') { sex = ' ♂'; } } picture = ''; if (person.picture) { picture = `<tr><td><img scale='true' src='${person.picture}' /></td></tr>`; } label = '<<table align="center" border="0" cellpadding="0" cellspacing="2" width="4">' + `${picture}` + '<tr><td align="center">' + `${person.name || person.display || id}${sex}</td></tr>`; if (person.fullname) { label += '<tr><td align="center">' + '<font point-size="10" color="#aaaaaa">' + `${person.fullname || person.name}` + `</font></td></tr>`; } if (person.date_of_birth || person.date_of_death) { if (person.date_of_birth && person.date_of_death) { lifespan = `${ '*' + new Date(person.date_of_birth).getFullYear() }${'†' + new Date(person.date_of_death).getFullYear()}`; } else if (person.date_of_birth) { lifespan = `${ '*' + new Date(person.date_of_birth).getFullYear() }`; } else { lifespan = `${ '†' + new Date(person.date_of_death).getFullYear() }`; } label += '<tr><td align="center">' + '<font point-size="10" color="#aaaaaa">' + `${lifespan}</font></td></tr>`; } label += '</table>>'; } else { label = id; } return [ `"${id}" [`, 'tooltip=' + '"' + renderPersonTooltip(person) + '"', { indent: [ applyStyle(data, person.class || [], { before: { label, href, }, }), ], }, ']', ]; } function renderPersonTooltip(person) { let txt = ''; txt += person.first_name + ' ' || ''; txt += person.last_name || ''; txt += '\n'; txt += 'Added ' + new Date(person.created_at).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', }); txt += '\n'; txt += person.date_of_birth ? 'Born ' + new Date(person.date_of_birth).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', }) + ' ' : ''; txt += person.date_of_birth && person.date_of_death ? ' -- died ' + new Date(person.date_of_death).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', }) : ''; txt += !person.date_of_birth && person.date_of_death ? ' Died ' + new Date(person.date_of_death).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', }) : ''; txt += '\n'; txt += person.comment ? person.comment.slice(0, 50) + '...' : ''; txt += '\n'; return txt; } /* * For comments */ function summarizeFamily(family) { const parents = [] .concat(family.parents || []) .concat(family.parents2 || []) .filter(Boolean); const children = [] .concat(family.children || []) .concat(family.children2 || []) .filter(Boolean); return `[${parents.join(', ')}] -> [${children.join(', ')}]`; } /* * Renders a family subgraph */ function renderFamily(data, house, family, path) { const slug = slugify(path); const color = COLORS[getId('family') % COLORS.length]; const parents = family.parents || []; const parents2 = family.parents2 || []; const children = family.children || []; const children2 = family.children2 || []; const housename = family.house; const city = family.city || "" const country = family.country || "" const hasParents = parents.length + parents2.length > 0; const hasChildren = children.length + children2.length > 0; const hasManyChildren = children.length + children.length > 1; const union = `union_${slug}`; const kids = `siblings_${slug}`; return [ '', `subgraph cluster_family_${slug} {`, style([':family']), { indent: [ housename && renderHousePrelude(), // renderPeople(), renderSubFamilies(), '', `# Family ${summarizeFamily(family)}`, LINE2, '', hasParents && renderParents(), hasParents && hasChildren && renderLink(), hasChildren && renderKids(), hasManyChildren > 1 && renderKidLinks(), ], }, '}', ]; function style(classes, before) { return { indent: applyStyle(data, classes, { before: before || {} }) }; } function renderPeople() { const list = [].concat(parents).concat(children); return `{${list.map(escape).join('; ')}}`; } function renderHousePrelude() { let location_label = city && country ? ` |<i>${city}, ${country}</i>`: "" let label = `<<b>${housename}</b>${location_label}>`; let labelhref = family.links && family.links[0]; return [applyStyle(data, [':house'], { before: { label, labelhref } })]; } function renderSubFamilies() { // Reverse the families, because we assume people put "deeper" families last. // You want to render the deeper families first so that their parents are placed // in those families, rather than the parent families. const families = [].concat(family.families || []).reverse(); return families.map((f, idx) => renderFamily(data, house, f, path.concat(idx)) ); } function renderParents() { return [ `${union} [`, style([':union'], { fillcolor: color }), ']', '', parents.length > 0 && [ `{${parents.map(escape).join(', ')}} -> ${union} [`, style([':parent-link'], { color }), ']', ], parents2.length > 0 && [ `{${parents2.map(escape).join(', ')}} -> ${union} [`, style([':parent-link', ':parent2-link'], { color }), ']', ], ]; } function renderLink() { return [ `${union} -> ${kids} [`, style([':parent-link', ':parent-child-link'], { color: color }), ']', ]; } function renderKids() { return [ `${kids} [`, style([':children'], { fillcolor: color }), `]`, children.length > 0 && [ `${kids} -> {${children.map(escape).join(', ')}} [`, style([':child-link'], { color }), ']', ], children2.length > 0 && [ `${kids} -> {${children2.map(escape).join(', ')}} [`, style([':child-link', ':child2-link'], { color }), ']', ], ]; } function renderKidLinks() { return [ `{"${children.concat(children2).join('" -> "')}" [`, { indent: [ applyStyle(data, [':child-links'], { before: { style: 'invis', }, }), ], }, ']', ]; } } /* * Escapes a name into a node name. */ function escape(str) { if (/^[A-Za-z]+$/.test(str)) { return str; } else { return JSON.stringify(str); } } /* * Export */ module.exports = render;