tarinai/js/family_graph.js
2026-07-01 14:41:05 +09:00

127 lines
5 KiB
JavaScript

"use strict";
(function (global) {
function buildGraph(nodes = [], opts = {}) {
const idOf = opts.idOf || (n => n?.id);
const edgesOf = opts.edgesOf || ((n) => [...(n?.parents || []), ...(n?.children || [])]);
const byId = new Map(nodes.filter(Boolean).map(n => [idOf(n), n]).filter(([id]) => id));
const graph = new Map([...byId.keys()].map(id => [id, new Set()]));
const connect = (a, b) => {
if (!a || !b || a === b || !graph.has(a) || !graph.has(b)) return;
graph.get(a).add(b);
graph.get(b).add(a);
};
for (const node of byId.values()) {
const id = idOf(node);
for (const next of edgesOf(node, byId)) connect(id, next);
}
return { graph, byId };
}
function connectedComponents(nodes = [], opts = {}) {
const { graph, byId } = buildGraph(nodes, opts);
const sort = opts.sort || null;
const nodeList = [...byId.values()].sort(sort || (() => 0));
const seen = new Set();
const comps = [];
for (const node of nodeList) {
const start = opts.idOf ? opts.idOf(node) : node.id;
if (!start || seen.has(start)) continue;
const stack = [start];
const comp = [];
seen.add(start);
while (stack.length) {
const id = stack.pop();
const item = byId.get(id);
if (item) comp.push(item);
for (const next of graph.get(id) || []) {
if (seen.has(next)) continue;
seen.add(next);
stack.push(next);
}
}
if (!opts.filter || opts.filter(comp)) comps.push(sort ? comp.sort(sort) : comp);
}
return comps;
}
function addParentEdge(childId, parentId, known, parentsByChild, childrenByParent) {
if (!childId || !parentId || childId === parentId || !known?.has?.(childId) || !known.has(parentId)) return false;
if (!parentsByChild.has(childId)) parentsByChild.set(childId, new Set());
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, new Set());
parentsByChild.get(childId).add(parentId);
childrenByParent.get(parentId).add(childId);
return true;
}
function buildRelationIndex(family = {}) {
const known = new Set(Object.keys(family || {}));
const parentsByChild = new Map();
const childrenByParent = new Map();
const addParent = (childId, parentId) => addParentEdge(childId, parentId, known, parentsByChild, childrenByParent);
for (const n of Object.values(family || {})) {
if (!n?.id) continue;
for (const parentId of n.parents || []) addParent(n.id, parentId);
for (const childId of n.children || []) addParent(childId, n.id);
}
return { parentsByChild, childrenByParent };
}
function relationAllowsParentChild(family = {}, childId, parentId) {
const child = family?.[childId];
const parent = family?.[parentId];
if (!child || !parent) return false;
const childListsParent = Array.isArray(child.parents) && child.parents.includes(parentId);
const parentListsChild = Array.isArray(parent.children) && parent.children.includes(childId);
return childListsParent || parentListsChild;
}
function relatedNodesFromIndex(family = {}, relationIndex = buildRelationIndex(family)) {
const relatedIds = new Set();
for (const n of Object.values(family || {})) {
if (!n?.id) continue;
const parents = relationIndex.parentsByChild.get(n.id) || new Set();
const children = relationIndex.childrenByParent.get(n.id) || new Set();
if (parents.size || children.size) relatedIds.add(n.id);
for (const id of parents) relatedIds.add(id);
for (const id of children) relatedIds.add(id);
}
return Array.from(relatedIds).map(id => family[id]).filter(Boolean);
}
function buildLineageGraph(family = {}, nodes = relatedNodesFromIndex(family), relationIndex = buildRelationIndex(family)) {
const graph = new Map();
for (const n of nodes || []) if (n?.id) graph.set(n.id, new Set());
const addGraphEdge = (a, b) => {
if (!a || !b || !graph.has(a) || !graph.has(b)) return;
graph.get(a).add(b);
graph.get(b).add(a);
};
for (const n of nodes || []) {
if (!n?.id) continue;
const parentCandidates = new Set((n.parents || []).filter(Boolean));
for (const parentId of relationIndex.parentsByChild.get(n.id) || []) parentCandidates.add(parentId);
for (const parentId of parentCandidates) {
if (parentId !== n.id && relationAllowsParentChild(family, n.id, parentId)) addGraphEdge(n.id, parentId);
}
const childCandidates = new Set((n.children || []).filter(Boolean));
for (const childId of relationIndex.childrenByParent.get(n.id) || []) childCandidates.add(childId);
for (const childId of childCandidates) {
if (childId !== n.id && relationAllowsParentChild(family, childId, n.id)) addGraphEdge(n.id, childId);
}
}
return graph;
}
global.TarinaiFamilyGraph = {
connectedComponents,
addParentEdge,
buildRelationIndex,
buildLineageGraph,
relatedNodesFromIndex,
relationAllowsParentChild,
};
})(typeof window !== "undefined" ? window : globalThis);