/
nek5132
/
Genius
Обзор
Документация
Войти
/
nek5132
/
Genius
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/core/GraphState.js
905 строк
29 KB
Peter Kosov
feat: add anchored person labels and selection card
13 апр 2026, 17:15
13 апр 2026, 17:15
43a6ee1
Код
Авторство
О чём код?
// Контракт данных для графического движка import { VISUAL_TOKENS } from './visualTokens.js'; export const LAYOUT_VERSIONS = { ORTHO_TREE_V1: 'ortho-tree-v1', RADIAL_TREE_V1: 'radial-tree-v1' }; export const STYLE_VERSIONS = { WHITE_GRAY_V1: 'white-gray-v1' }; export const LAYOUT_PARAMS = { 'ortho-tree-v1': { coupleGap: 1.5, genGap: 2.5, familyGap: 1.0, zSpread: 0.8, rotateAngle: Math.PI / 2, minNodeSeparation: 0.3 }, // Forest layout tuning (optional feature) 'forest-layout': { hubOpacity: 0.5, gridEnabled: true, gridSize: 50, densityFactor: 1.0, maxForestDepth: 6, COMPONENT_GAP: 1000 }, 'radial-tree-v1': { radiusStep: 3.0, angleStep: Math.PI / 4, rootRadius: 0, minNodeSeparation: 20 } }; export const STYLE_PARAMS = { 'white-gray-v1': { maleNodeSize: 1.0, femaleNodeSize: 1.0, hubRadius: 0.4, maleColor: VISUAL_TOKENS.color.person.male.light, femaleColor: VISUAL_TOKENS.color.person.female.light, hubColor: VISUAL_TOKENS.color.hub.light, edgeColor: VISUAL_TOKENS.color.edge.light, edgeWidth: 0.05 } }; const cloneState = (state) => { if (typeof structuredClone === 'function') return structuredClone(state); return JSON.parse(JSON.stringify(state)); }; const asArray = (value) => (Array.isArray(value) ? value : []); const uniqueBy = (items, keyFn) => { const seen = new Set(); return items.filter(item => { const key = keyFn(item); if (seen.has(key)) return false; seen.add(key); return true; }); }; export function normalizeGenealogyState(state) { const draft = cloneState(state); if (!draft?.groups || draft.config?.modelProfile !== 'genealogy') return draft; const entityById = new Map(asArray(draft.entities).map(entity => [entity.id, entity])); const familyById = new Map(asArray(draft.groups).map(group => [group.id, group])); draft.groups = asArray(draft.groups).map(group => { const attrs = group.attrs || {}; const childrenIds = uniqueBy(asArray(attrs.childrenIds), id => id); const memberIds = uniqueBy(asArray(group.memberIds), id => id); return { ...group, memberIds, attrs: { ...attrs, husbandId: attrs.husbandId || null, wifeId: attrs.wifeId || null, childrenIds } }; }); const spouseFamilyByPerson = new Map(); draft.groups.forEach(family => { const attrs = family.attrs || {}; if (attrs.husbandId) spouseFamilyByPerson.set(attrs.husbandId, { familyId: family.id, roleInChildFamily: 'husband' }); if (attrs.wifeId) spouseFamilyByPerson.set(attrs.wifeId, { familyId: family.id, roleInChildFamily: 'wife' }); }); const derivedRelations = []; const seen = new Set(); draft.groups.forEach(parentFamily => { asArray(parentFamily.attrs?.childrenIds).forEach(childId => { const childFamily = spouseFamilyByPerson.get(childId); if (!childFamily) return; const parentFamilyRecord = familyById.get(parentFamily.id); if (!parentFamilyRecord) return; const key = `${parentFamily.id}->${childFamily.familyId}:${childId}`; if (seen.has(key)) return; seen.add(key); derivedRelations.push({ id: `rel-${parentFamily.id}-${childFamily.familyId}-${childId}`, kind: 'parent-child-family-link', fromId: parentFamily.id, toId: childFamily.familyId, attrs: { parentFamilyId: parentFamily.id, childPersonId: childId, childFamilyId: childFamily.familyId, roleInChildFamily: childFamily.roleInChildFamily } }); }); }); const preservedRelations = asArray(draft.relations).filter(rel => rel.kind !== 'parent-child-family-link'); const canonicalRelations = derivedRelations.filter(rel => { const parent = familyById.get(rel.attrs.parentFamilyId); const child = familyById.get(rel.attrs.childFamilyId); return !!parent && !!child && asArray(parent.attrs?.childrenIds).includes(rel.attrs.childPersonId); }); draft.relations = [...preservedRelations, ...canonicalRelations]; return draft; } // Валидация GraphState export function validateGraphState(state) { const errors = []; if (!state.config?.modelProfile) { errors.push('config.modelProfile is required'); } if (!state.config?.mode || !['2d', '3d'].includes(state.config.mode)) { errors.push('config.mode must be 2d or 3d'); } if (!state.config?.layout || !['vertical-2d', 'vertical-3d', 'radial-2d', 'radial-3d'].includes(state.config.layout)) { errors.push('config.layout must be one of vertical-2d, vertical-3d, radial-2d, radial-3d'); } const allIds = new Set(); const checkId = (id) => { if (allIds.has(id)) { errors.push(`duplicate id: ${id}`); } else { allIds.add(id); } }; state.entities.forEach((e) => checkId(e.id)); state.groups.forEach((g) => checkId(g.id)); state.relations.forEach((r) => checkId(r.id)); const entityIds = new Set(state.entities.map((e) => e.id)); const groupIds = new Set(state.groups.map((g) => g.id)); state.groups.forEach((g) => { if (g.attrs.husbandId && !entityIds.has(g.attrs.husbandId)) { errors.push(`Missing husband entity: ${g.attrs.husbandId}`); } if (g.attrs.wifeId && !entityIds.has(g.attrs.wifeId)) { errors.push(`Missing wife entity: ${g.attrs.wifeId}`); } g.attrs.childrenIds.forEach((childId) => { if (!entityIds.has(childId)) { errors.push(`Missing child entity: ${childId}`); } }); }); state.relations.forEach((r) => { if (!groupIds.has(r.fromId)) { errors.push(`Missing from group: ${r.fromId}`); } if (!groupIds.has(r.toId)) { errors.push(`Missing to group: ${r.toId}`); } }); // Validate ParentChildFamilyLink constraints if (state.config?.modelProfile === 'genealogy') { const childToParentFamily = new Map(); const childToParentLink = new Map(); state.relations.forEach((r) => { if (r.attrs?.childPersonId) { if (childToParentFamily.has(r.attrs.childPersonId)) { errors.push(`Person ${r.attrs.childPersonId} has more than one parent family link`); } childToParentFamily.set(r.attrs.childPersonId, r.attrs.parentFamilyId); const linkKey = r.attrs.parentFamilyId + '->' + r.attrs.childFamilyId; if (childToParentLink.has(linkKey)) { errors.push(`Duplicate ParentLink: ${linkKey}`); } childToParentLink.set(linkKey, r.id); } }); state.groups.forEach((g) => { const parentLink = state.relations.find(r => r.attrs?.childFamilyId === g.id); if (parentLink) { const childPersonId = parentLink.attrs?.childPersonId; if (childPersonId) { const isHusband = g.attrs?.husbandId === childPersonId; const isWife = g.attrs?.wifeId === childPersonId; if (!isHusband && !isWife) { errors.push(`childPersonId ${childPersonId} must be husband or wife in family ${g.id}`); } const parentFamily = state.groups.find(f => f.id === parentLink.attrs?.parentFamilyId); if (parentFamily && !parentFamily.attrs?.childrenIds?.includes(childPersonId)) { errors.push(`childPersonId ${childPersonId} not in parent's childrenIds`); } if (parentLink.attrs?.roleInChildFamily === 'husband' && !isHusband) { errors.push(`roleInChildFamily is 'husband' but person is not husband in ${g.id}`); } if (parentLink.attrs?.roleInChildFamily === 'wife' && !isWife) { errors.push(`roleInChildFamily is 'wife' but person is not wife in ${g.id}`); } } } }); // Check for cycles using DFS const visited = new Set(); const recursionStack = new Set(); const hasCycle = (familyId, path = []) => { if (recursionStack.has(familyId)) { errors.push(`Cycle detected in family tree: ${path.join(' -> ')} -> ${familyId}`); return true; } if (visited.has(familyId)) return false; visited.add(familyId); recursionStack.add(familyId); const family = state.groups.find(g => g.id === familyId); if (family) { for (const childId of (family.attrs?.childrenIds || [])) { const childFamilyId = [...state.relations] .find(r => r.attrs?.childPersonId === childId)?.attrs?.childFamilyId; if (childFamilyId) { if (hasCycle(childFamilyId, [...path, familyId])) return true; } } } recursionStack.delete(familyId); return false; }; const rootFamilies = state.groups.filter(g => { return !state.relations.some(r => r.attrs?.childFamilyId === g.id); }); rootFamilies.forEach(f => hasCycle(f.id)); } return { valid: errors.length === 0, errors, }; } export function generateGenealogyData(generations = 4, options = {}) { const { complexFamilies = false } = options; const entities = []; const groups = []; const relations = []; let idCounter = 0; const maleFirstNames = ['Алексей', 'Даниил', 'Иван', 'Марк', 'Павел', 'Николай']; const femaleFirstNames = ['Анна', 'Елена', 'Мария', 'София', 'Юлия', 'Нина']; const lastNames = ['Петров', 'Иванов', 'Соколов', 'Смирнов', 'Кузнецов', 'Лебедев']; const personHasParentFamily = (personId) => { return groups.some(group => (group.attrs?.childrenIds || []).includes(personId)); }; const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; const pick = (items) => items[randInt(0, items.length - 1)]; const createPerson = (gender, gen, nameSeed = {}) => { const id = `p${idCounter++}`; const firstName = nameSeed.firstName || (gender === 'male' ? pick(maleFirstNames) : pick(femaleFirstNames)); const lastName = nameSeed.lastName || pick(lastNames); entities.push({ id, kind: "person", attrs: { gender, firstName, lastName, birthYear: 1940 + gen * 25 + randInt(0, 10), }, }); return id; }; const createFamily = (husbandId, wifeId, childrenIds) => { const familyId = `f${idCounter++}`; const memberIds = []; if (husbandId) memberIds.push(husbandId); if (wifeId) memberIds.push(wifeId); memberIds.push(...childrenIds); groups.push({ id: familyId, kind: "family", memberIds, attrs: { husbandId: husbandId || null, wifeId: wifeId || null, childrenIds: [...childrenIds], turned: false, compact: false, }, }); return familyId; }; const createAncestors = (personId, personGender, depth, parentFamilyId) => { if (depth <= 0) return; if (personHasParentFamily(personId)) return; const ancestorGen = -depth; const child = entities.find(e => e.id === personId); const childLastName = child?.attrs?.lastName || pick(lastNames); const fatherId = createPerson('male', ancestorGen, { lastName: childLastName }); const motherId = createPerson('female', ancestorGen, { lastName: childLastName }); // Create ancestor family and link it to the current family. // The ancestor family is the parent; the current family is the child. const famId = createFamily(fatherId, motherId, [personId]); // Link ancestor family -> current family (correct direction). if (parentFamilyId) { linkFamilies(famId, parentFamilyId, personId, personGender === 'male' ? 'husband' : 'wife'); } if (depth > 1 && Math.random() < 0.8) { createAncestors(fatherId, 'male', depth - 1, famId); createAncestors(motherId, 'female', depth - 1, famId); } }; const linkFamilies = (parentFamilyId, childFamilyId, childPersonId, roleInChildFamily) => { relations.push({ id: `r${idCounter++}`, kind: 'parent-child-family-link', fromId: parentFamilyId, toId: childFamilyId, attrs: { parentFamilyId, childPersonId, childFamilyId, roleInChildFamily } }); }; // Root family (generation 0) const rootHusband = createPerson('male', 0); const rootWife = createPerson('female', 0); const rootFamilyId = createFamily(rootHusband, rootWife, [], null, null, null); // Build tree generation by generation let prevGenFamilies = [rootFamilyId]; for (let g = 1; g < generations; g++) { const nextGenFamilies = []; for (const parentFamilyId of prevGenFamilies) { const parentFamily = groups.find(f => f.id === parentFamilyId); if (!parentFamily) continue; if (complexFamilies && g >= 1 && g <= generations - 2) { const husbandId = parentFamily.attrs.husbandId; const wifeId = parentFamily.attrs.wifeId; const depth = 3; if (husbandId && Math.random() < 0.5) { createAncestors(husbandId, 'male', depth, parentFamilyId); } if (wifeId && Math.random() < 0.5) { createAncestors(wifeId, 'female', depth, parentFamilyId); } } let childFamiliesCount; if (complexFamilies && g >= 2 && g <= generations - 2 && Math.random() < 0.3) { childFamiliesCount = randInt(3, 5); } else { childFamiliesCount = randInt(1, 2); } const parentChildrenIds = []; for (let cf = 0; cf < childFamiliesCount; cf++) { const childGender = Math.random() > 0.5 ? 'male' : 'female'; const lastName = parentFamily.attrs?.husbandId ? entities.find(e => e.id === parentFamily.attrs.husbandId)?.attrs?.lastName : entities.find(e => e.id === parentFamily.attrs?.wifeId)?.attrs?.lastName; const childId = createPerson(childGender, g, { lastName: lastName || pick(lastNames) }); const spouseGender = childGender === 'male' ? 'female' : 'male'; const spouseId = createPerson(spouseGender, g, { lastName: lastName || pick(lastNames) }); const roleInChild = childGender === 'male' ? 'husband' : 'wife'; let grandchildrenCount; if (complexFamilies && g >= 1 && g <= generations - 2 && Math.random() < 0.4) { grandchildrenCount = g < generations - 1 ? randInt(3, 6) : 0; } else { grandchildrenCount = g < generations - 1 ? randInt(0, 2) : 0; } const grandchildrenIds = []; for (let gc = 0; gc < grandchildrenCount; gc++) { grandchildrenIds.push(createPerson(Math.random() > 0.5 ? 'male' : 'female', g + 1, { lastName: lastName || pick(lastNames) })); } const childFamilyId = createFamily( childGender === 'male' ? childId : spouseId, childGender === 'female' ? childId : spouseId, grandchildrenIds, null, null, null ); linkFamilies(parentFamilyId, childFamilyId, childId, roleInChild); parentChildrenIds.push(childId); nextGenFamilies.push(childFamilyId); } parentFamily.attrs.childrenIds = parentChildrenIds; parentFamily.memberIds = [...parentFamily.memberIds.filter(id => id !== undefined), ...parentChildrenIds]; } prevGenFamilies = nextGenFamilies; } return { specVersion: "1.1", config: { modelProfile: "genealogy", mode: "3d", layout: "vertical-3d", layoutVersion: LAYOUT_VERSIONS.ORTHO_TREE_V1, styleVersion: STYLE_VERSIONS.WHITE_GRAY_V1 }, entities, groups, relations, }; } export function createEmptyGenealogyState() { return { specVersion: '1.1', config: { modelProfile: 'genealogy', mode: '3d', layout: 'vertical-3d', layoutVersion: LAYOUT_VERSIONS.ORTHO_TREE_V1, styleVersion: STYLE_VERSIONS.WHITE_GRAY_V1 }, entities: [], groups: [], relations: [] }; } export function generateRootFamilyData(options = {}) { const firstChild = generateGenealogyData(1, options); const state = createEmptyGenealogyState(); state.entities = firstChild.entities; state.groups = firstChild.groups; state.relations = firstChild.relations; return state; } const normalizeGender = (value) => { if (value === 'male' || value === 'female') return value; return 'male'; }; const sortById = (items) => [...items].sort((a, b) => a.id.localeCompare(b.id)); export function serializeGenealogyDSL(state) { const normalized = normalizeGenealogyState(state); if (normalized?.config?.modelProfile !== 'genealogy') { throw new Error('Genealogy DSL only supports genealogy modelProfile'); } const lines = ['graph genealogy']; if (normalized.specVersion) lines.push(`meta specVersion ${JSON.stringify(normalized.specVersion)}`); if (normalized.config?.mode) lines.push(`meta mode ${JSON.stringify(normalized.config.mode)}`); if (normalized.config?.layout) lines.push(`meta layout ${JSON.stringify(normalized.config.layout)}`); if (normalized.config?.layoutVersion) lines.push(`meta layoutVersion ${JSON.stringify(normalized.config.layoutVersion)}`); if (normalized.config?.styleVersion) lines.push(`meta styleVersion ${JSON.stringify(normalized.config.styleVersion)}`); sortById(normalized.entities).forEach((entity) => { const attrs = entity.attrs || {}; const fields = [ `gender=${JSON.stringify(normalizeGender(attrs.gender))}`, attrs.birthYear !== null && attrs.birthYear !== undefined ? `birthYear=${JSON.stringify(attrs.birthYear)}` : null ].filter(Boolean).join(' '); lines.push(`person ${entity.id}${fields ? ` ${fields}` : ''}`); }); sortById(normalized.groups).forEach((group) => { const attrs = group.attrs || {}; const fields = [ attrs.husbandId ? `husband=${JSON.stringify(attrs.husbandId)}` : null, attrs.wifeId ? `wife=${JSON.stringify(attrs.wifeId)}` : null, attrs.childrenIds?.length ? `children=${JSON.stringify(attrs.childrenIds.join(','))}` : null, attrs.turned ? 'turned=true' : null, attrs.compact ? 'compact=true' : null ].filter(Boolean).join(' '); lines.push(`family ${group.id}${fields ? ` ${fields}` : ''}`); }); sortById(normalized.relations).forEach((relation) => { const attrs = relation.attrs || {}; if (relation.kind !== 'parent-child-family-link') return; lines.push([ 'link', relation.id, `from=${JSON.stringify(relation.fromId)}`, `to=${JSON.stringify(relation.toId)}`, `parentFamily=${JSON.stringify(attrs.parentFamilyId)}`, `childPerson=${JSON.stringify(attrs.childPersonId)}`, `childFamily=${JSON.stringify(attrs.childFamilyId)}`, `role=${JSON.stringify(attrs.roleInChildFamily)}` ].join(' ')); }); return `${lines.join('\n')}\n`; } const parseDSLValue = (value) => { const trimmed = value.trim(); if (!trimmed) return ''; if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return JSON.parse(trimmed); } return trimmed; }; const parseDSLFields = (tokens) => { const fields = {}; tokens.forEach((token) => { const [key, rawValue] = token.split('='); if (!key || rawValue === undefined) return; fields[key] = parseDSLValue(rawValue); }); return fields; }; export function parseGenealogyDSL(text) { const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean); const entities = []; const groups = []; const relations = []; const meta = {}; lines.forEach((line) => { if (line.startsWith('#')) return; const tokens = line.match(/(?:[^\s"]+|"[^"]*"|'[^']*')+/g) || []; if (tokens.length === 0) return; const [kind, ...rest] = tokens; if (kind === 'graph') return; if (kind === 'meta') { const [key, ...valueTokens] = rest; if (!key) return; meta[key] = parseDSLValue(valueTokens.join(' ')); return; } if (kind === 'person') { const [id, ...fieldTokens] = rest; const fields = parseDSLFields(fieldTokens); entities.push({ id, kind: 'person', attrs: { gender: normalizeGender(fields.gender), birthYear: fields.birthYear === undefined ? null : Number(fields.birthYear) } }); return; } if (kind === 'family') { const [id, ...fieldTokens] = rest; const fields = parseDSLFields(fieldTokens); const childrenIds = typeof fields.children === 'string' && fields.children ? fields.children.split(',').filter(Boolean) : []; const husbandId = fields.husband || null; const wifeId = fields.wife || null; groups.push({ id, kind: 'family', memberIds: [husbandId, wifeId, ...childrenIds].filter(Boolean), attrs: { husbandId, wifeId, childrenIds, turned: fields.turned === 'true', compact: fields.compact === 'true' } }); return; } if (kind === 'link') { const [id, ...fieldTokens] = rest; const fields = parseDSLFields(fieldTokens); relations.push({ id, kind: 'parent-child-family-link', fromId: fields.from, toId: fields.to, attrs: { parentFamilyId: fields.parentFamily, childPersonId: fields.childPerson, childFamilyId: fields.childFamily, roleInChildFamily: fields.role } }); } }); const specVersion = meta.specVersion || '1.1'; const config = { modelProfile: 'genealogy', mode: meta.mode || '3d', layout: meta.layout || 'vertical-3d', layoutVersion: meta.layoutVersion || LAYOUT_VERSIONS.ORTHO_TREE_V1, styleVersion: meta.styleVersion || STYLE_VERSIONS.WHITE_GRAY_V1 }; return normalizeGenealogyState({ specVersion, config, entities, groups, relations }); } export function parseGEDCOM(text) { const entities = []; const groups = []; const relations = []; let idCounter = 0; const personMap = new Map(); const familyMap = new Map(); const lines = text.split('\n').map(l => l.trim()).filter(l => l); let currentPerson = null; let currentFamily = null; let currentLevel = 0; for (const line of lines) { const parts = line.split(' '); const level = parseInt(parts[0]); const tag = parts[1]; const value = parts.slice(2).join(' '); if (level === 0) { if (tag === 'INDI') { currentPerson = { id: value, attrs: { gender: 'male', birthYear: null } }; personMap.set(value, currentPerson); } else if (tag === 'FAM') { currentFamily = { id: value, husbandId: null, wifeId: null, childrenIds: [] }; familyMap.set(value, currentFamily); } } else if (currentPerson && level === 1) { if (tag === 'NAME') { // NAME is parsed but discarded to keep data-only state. } else if (tag === 'SEX') { currentPerson.attrs.gender = value === 'F' ? 'female' : 'male'; } else if (tag === 'BIRT') { } else if (tag === 'FAMC') { currentPerson.parentFamilyId = value; } else if (tag === 'FAMS') { currentPerson.spouseFamilyId = value; } } else if (currentPerson && level === 2 && tag === 'DATE') { const yearMatch = value.match(/(\d{4})/); if (yearMatch) currentPerson.attrs.birthYear = parseInt(yearMatch[1]); } else if (currentFamily && level === 1) { if (tag === 'HUSB') currentFamily.husbandId = value; else if (tag === 'WIFE') currentFamily.wifeId = value; else if (tag === 'CHIL') currentFamily.childrenIds.push(value); } } personMap.forEach((p, id) => { entities.push({ id: `p${idCounter++}`, kind: 'person', attrs: { ...p.attrs } }); }); const idTranslation = new Map(); let idx = 0; personMap.forEach((p, originalId) => { idTranslation.set(originalId, `p${idx++}`); }); familyMap.forEach((f, famId) => { const familyId = `f${idCounter++}`; const husbandId = f.husbandId ? idTranslation.get(f.husbandId) : null; const wifeId = f.wifeId ? idTranslation.get(f.wifeId) : null; const childrenIds = f.childrenIds.map(c => idTranslation.get(c)).filter(Boolean); const memberIds = [husbandId, wifeId, ...childrenIds].filter(Boolean); groups.push({ id: familyId, kind: 'family', memberIds, attrs: { husbandId, wifeId, childrenIds, turned: false, compact: false } }); }); familyMap.forEach((f, famId) => { const familyId = [...groups.keys()].find(k => { const g = groups[k]; const origHusband = [...personMap.entries()].find(([_, p]) => idTranslation.get(p.id) === g.attrs.husbandId)?.[0]; const origWife = [...personMap.entries()].find(([_, p]) => idTranslation.get(p.id) === g.attrs.wifeId)?.[0]; return (f.husbandId && origHusband === f.husbandId) || (f.wifeId && origWife === f.wifeId); }); f.childrenIds.forEach(childId => { const translatedChildId = idTranslation.get(childId); const childPerson = personMap.get(childId); if (childPerson && childPerson.parentFamilyId) { const parentFamId = [...groups.keys()].find(k => { const g = groups[k]; const origHusband = [...personMap.entries()].find(([_, p]) => idTranslation.get(p.id) === g.attrs.husbandId)?.[0]; const origWife = [...personMap.entries()].find(([_, p]) => idTranslation.get(p.id) === g.attrs.wifeId)?.[0]; return childPerson.parentFamilyId === origHusband || childPerson.parentFamilyId === origWife; }); if (parentFamId) { relations.push({ id: `r${idCounter++}`, kind: 'parent-child-family-link', fromId: parentFamId, toId: familyId, attrs: { parentFamilyId: parentFamId, childPersonId: translatedChildId, childFamilyId: familyId, roleInChildFamily: childPerson.attrs.gender === 'female' ? 'wife' : 'husband' } }); } } }); }); return { specVersion: '1.1', config: { modelProfile: 'genealogy', mode: '3d', layout: 'vertical-3d' }, entities, groups, relations }; } export function generatePhylogenyData(depth = 3) { const entities = []; const relations = []; let idCounter = 0; const SPECIES = ["Homo sapiens", "Pan troglodytes", "Gorilla gorilla", "Pongo pygmaeus", "Pan paniscus", "Gorilla beringei", "Pongo abelii", "Homo neanderthalensis", "Homo erectus", "Australopithecus afarensis"]; const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; const buildTree = (parentId, currentDepth) => { if (currentDepth >= depth) return; const branchCount = Math.floor(Math.random() * 2) + 2; for (let i = 0; i < branchCount; i++) { const childId = `n${idCounter++}`; const isLeaf = currentDepth === depth - 1; const branchLength = Math.random() * 0.5 + 0.1; entities.push({ id: childId, kind: "phylo-node", attrs: { label: isLeaf ? rand(SPECIES) : `Node ${childId}`, isLeaf, branchLength: parseFloat(branchLength.toFixed(2)) } }); relations.push({ id: `e${idCounter++}`, kind: "phylo-edge", fromId: parentId, toId: childId, attrs: { branchLength: parseFloat(branchLength.toFixed(2)) } }); buildTree(childId, currentDepth + 1); } }; const rootId = `n${idCounter++}`; entities.push({ id: rootId, kind: "phylo-node", attrs: { label: "Root", isRoot: true, isLeaf: false } }); buildTree(rootId, 0); return { specVersion: "1.1", config: { modelProfile: "phylogeny", mode: "3d", layout: "vertical-3d" }, entities, groups: [], relations, }; } export function generateGenericTreeData(depth = 3) { const entities = []; const relations = []; let idCounter = 0; const CATEGORIES = ["data", "process", "entity", "concept", "event", "relation"]; const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; const buildTree = (parentId, currentDepth) => { if (currentDepth >= depth) return; const childCount = Math.floor(Math.random() * 3) + 1; for (let i = 0; i < childCount; i++) { const childId = `n${idCounter++}`; const isLeaf = currentDepth === depth - 1; const category = isLeaf ? "leaf" : rand(CATEGORIES); entities.push({ id: childId, kind: "tree-node", attrs: { label: `Node ${childId}`, category: isLeaf ? "leaf" : category } }); relations.push({ id: `r${idCounter++}`, kind: "tree-edge", fromId: parentId, toId: childId, attrs: {} }); buildTree(childId, currentDepth + 1); } }; const rootId = `n${idCounter++}`; entities.push({ id: rootId, kind: "tree-node", attrs: { label: "Root", category: "root" } }); buildTree(rootId, 0); return { specVersion: "1.1", config: { modelProfile: "generic-tree", mode: "3d", layout: "vertical-3d" }, entities, groups: [], relations, }; }