mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-11 21:39:06 +02:00
improve layout
This commit is contained in:
@@ -4,125 +4,197 @@ import type { Edge, Node } from '@xyflow/svelte';
|
||||
import { Position } from '@xyflow/svelte';
|
||||
|
||||
export class FamilyTree extends dagre.graphlib.Graph {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getLayoutedElements(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
nodeWidth: number,
|
||||
nodeHeight: number,
|
||||
direction = 'TB'
|
||||
): Layout {
|
||||
this.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 80, // Increased horizontal spacing between nodes
|
||||
ranksep: 120, // Increased vertical spacing between ranks
|
||||
marginx: 50,
|
||||
marginy: 50
|
||||
});
|
||||
this.setDefaultEdgeLabel(() => ({}));
|
||||
getLayoutedElements(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
nodeWidth: number,
|
||||
nodeHeight: number,
|
||||
direction = 'TB'
|
||||
): Layout {
|
||||
this.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 80,
|
||||
ranksep: 120,
|
||||
marginx: 50,
|
||||
marginy: 50
|
||||
});
|
||||
this.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
nodes.forEach((node) => {
|
||||
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
// Add nodes to dagre
|
||||
nodes.forEach((node) => {
|
||||
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
if (String(edge.data!.type).toLowerCase() === 'child') {
|
||||
this.setEdge(edge.source, edge.target);
|
||||
}
|
||||
});
|
||||
// Only add parent-child edges to dagre for hierarchical layout
|
||||
edges.forEach((edge) => {
|
||||
if (String(edge.data!.type).toLowerCase() === 'child') {
|
||||
this.setEdge(edge.source, edge.target);
|
||||
}
|
||||
});
|
||||
|
||||
dagre.layout(this);
|
||||
// Run dagre layout
|
||||
dagre.layout(this);
|
||||
|
||||
let newEdges: Edge[] = [];
|
||||
edges.forEach((edge) => {
|
||||
let newEdge = { ...edge };
|
||||
if (String(edge.data?.type).toLowerCase() === 'child') {
|
||||
newEdge.sourceHandle = 'child';
|
||||
newEdge.targetHandle = 'parent';
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'parent') {
|
||||
return;
|
||||
}
|
||||
const sourceNode = this.node(edge.source);
|
||||
const targetNode = this.node(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
if (String(edge.data?.type).toLowerCase() === 'sibling') {
|
||||
const padding = 80; // Increased distance between sibling and source
|
||||
const spouseWidth = nodeWidth;
|
||||
const existingNodesAtLevel = nodes
|
||||
.map((n) => ({ id: n.id, pos: this.node(n.id) }))
|
||||
.filter(({ pos }) => Math.abs(pos.y - sourceNode.y) < nodeHeight / 2); // same horizontal band
|
||||
// Collect taken x ranges
|
||||
const takenXRanges = existingNodesAtLevel.map(({ pos }) => ({
|
||||
from: pos.x - spouseWidth / 2,
|
||||
to: pos.x + spouseWidth / 2
|
||||
}));
|
||||
// Try placing sibling to the right
|
||||
let desiredX = sourceNode.x + nodeWidth + padding;
|
||||
// Check for collision
|
||||
const collides = (x: number) => {
|
||||
return takenXRanges.some(({ from, to }) => x > from && x < to);
|
||||
};
|
||||
// If right side collides, try left
|
||||
if (collides(desiredX)) {
|
||||
desiredX = sourceNode.x - (nodeWidth + padding);
|
||||
}
|
||||
// If both sides collide, push right until free
|
||||
while (collides(desiredX)) {
|
||||
desiredX += nodeWidth + padding;
|
||||
}
|
||||
targetNode.x = desiredX;
|
||||
targetNode.y = sourceNode.y;
|
||||
}
|
||||
if (String(edge.data?.type).toLowerCase() === 'spouse') {
|
||||
const padding = 30; // Closer distance between spouse and source
|
||||
const spouseWidth = nodeWidth;
|
||||
const existingNodesAtLevel = nodes
|
||||
.map((n) => ({ id: n.id, pos: this.node(n.id) }))
|
||||
.filter(({ pos }) => Math.abs(pos.y - sourceNode.y) < nodeHeight / 2); // same horizontal band
|
||||
// Collect taken x ranges
|
||||
const takenXRanges = existingNodesAtLevel.map(({ pos }) => ({
|
||||
from: pos.x - spouseWidth / 2,
|
||||
to: pos.x + spouseWidth / 2
|
||||
}));
|
||||
// Try placing spouse to the right
|
||||
let desiredX = sourceNode.x + nodeWidth + padding;
|
||||
// Check for collision
|
||||
const collides = (x: number) => {
|
||||
return takenXRanges.some(({ from, to }) => x > from && x < to);
|
||||
};
|
||||
// If right side collides, try left
|
||||
if (collides(desiredX)) {
|
||||
desiredX = sourceNode.x - (nodeWidth + padding);
|
||||
}
|
||||
// If both sides collide, push right until free
|
||||
while (collides(desiredX)) {
|
||||
desiredX += nodeWidth + padding;
|
||||
}
|
||||
targetNode.x = desiredX;
|
||||
targetNode.y = sourceNode.y;
|
||||
}
|
||||
newEdge.hidden = false;
|
||||
newEdge.type = 'familyEdge';
|
||||
newEdges.push(newEdge);
|
||||
});
|
||||
// Create maps for relationship analysis
|
||||
const spouseMap = new Map<string, string>();
|
||||
const siblingMap = new Map<string, string[]>();
|
||||
const childrenMap = new Map<string, string[]>();
|
||||
const parentsMap = new Map<string, string[]>();
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = this.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
type: 'personNode',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
// Build relationship maps
|
||||
edges.forEach((edge) => {
|
||||
const type = String(edge.data?.type).toLowerCase();
|
||||
if (type === 'spouse') {
|
||||
spouseMap.set(edge.source, edge.target);
|
||||
spouseMap.set(edge.target, edge.source);
|
||||
} else if (type === 'sibling') {
|
||||
if (!siblingMap.has(edge.source)) siblingMap.set(edge.source, []);
|
||||
if (!siblingMap.has(edge.target)) siblingMap.set(edge.target, []);
|
||||
siblingMap.get(edge.source)!.push(edge.target);
|
||||
siblingMap.get(edge.target)!.push(edge.source);
|
||||
} else if (type === 'child') {
|
||||
if (!childrenMap.has(edge.source)) childrenMap.set(edge.source, []);
|
||||
if (!parentsMap.has(edge.target)) parentsMap.set(edge.target, []);
|
||||
childrenMap.get(edge.source)!.push(edge.target);
|
||||
parentsMap.get(edge.target)!.push(edge.source);
|
||||
}
|
||||
});
|
||||
|
||||
return { Nodes: layoutedNodes, Edges: newEdges };
|
||||
}
|
||||
// Helper function to check if position is occupied
|
||||
const isPositionOccupied = (x: number, y: number, excludeId?: string): boolean => {
|
||||
return nodes.some(node => {
|
||||
if (excludeId && node.id === excludeId) return false;
|
||||
const nodePos = this.node(node.id);
|
||||
return Math.abs(nodePos.x - x) < nodeWidth && Math.abs(nodePos.y - y) < nodeHeight;
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to find free position near a reference point
|
||||
const findFreePosition = (refX: number, refY: number, excludeId?: string): { x: number, y: number } => {
|
||||
const padding = 30;
|
||||
const positions = [
|
||||
{ x: refX + nodeWidth + padding, y: refY }, // right
|
||||
{ x: refX - nodeWidth - padding, y: refY }, // left
|
||||
{ x: refX + (nodeWidth + padding) * 2, y: refY }, // far right
|
||||
{ x: refX - (nodeWidth + padding) * 2, y: refY }, // far left
|
||||
];
|
||||
|
||||
for (const pos of positions) {
|
||||
if (!isPositionOccupied(pos.x, pos.y, excludeId)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
// If all positions are taken, just go further right
|
||||
return { x: refX + (nodeWidth + padding) * 3, y: refY };
|
||||
};
|
||||
|
||||
// Position spouses next to each other
|
||||
const processedSpouses = new Set<string>();
|
||||
spouseMap.forEach((spouse, person) => {
|
||||
if (processedSpouses.has(person)) return;
|
||||
|
||||
const personNode = this.node(person);
|
||||
const spouseNode = this.node(spouse);
|
||||
|
||||
// Determine who should be the anchor (prefer the one with children or hierarchically positioned)
|
||||
const personHasChildren = childrenMap.has(person) && childrenMap.get(person)!.length > 0;
|
||||
const spouseHasChildren = childrenMap.has(spouse) && childrenMap.get(spouse)!.length > 0;
|
||||
|
||||
let anchorNode, mobileNode, anchorId, mobileId;
|
||||
if (personHasChildren && !spouseHasChildren) {
|
||||
anchorNode = personNode;
|
||||
mobileNode = spouseNode;
|
||||
anchorId = person;
|
||||
mobileId = spouse;
|
||||
} else if (!personHasChildren && spouseHasChildren) {
|
||||
anchorNode = spouseNode;
|
||||
mobileNode = personNode;
|
||||
anchorId = spouse;
|
||||
mobileId = person;
|
||||
} else {
|
||||
// Both or neither have children, use alphabetical order
|
||||
if (person < spouse) {
|
||||
anchorNode = personNode;
|
||||
mobileNode = spouseNode;
|
||||
anchorId = person;
|
||||
mobileId = spouse;
|
||||
} else {
|
||||
anchorNode = spouseNode;
|
||||
mobileNode = personNode;
|
||||
anchorId = spouse;
|
||||
mobileId = person;
|
||||
}
|
||||
}
|
||||
|
||||
// Position mobile spouse next to anchor
|
||||
const newPos = findFreePosition(anchorNode.x, anchorNode.y, mobileId);
|
||||
mobileNode.x = newPos.x;
|
||||
mobileNode.y = newPos.y;
|
||||
|
||||
processedSpouses.add(person);
|
||||
processedSpouses.add(spouse);
|
||||
});
|
||||
|
||||
// Position siblings
|
||||
const processedSiblings = new Set<string>();
|
||||
siblingMap.forEach((siblings, person) => {
|
||||
if (processedSiblings.has(person)) return;
|
||||
|
||||
const personNode = this.node(person);
|
||||
|
||||
siblings.forEach((sibling) => {
|
||||
if (processedSiblings.has(sibling)) return;
|
||||
|
||||
const siblingNode = this.node(sibling);
|
||||
|
||||
// If sibling is not a spouse of someone at the same level, position as sibling
|
||||
const siblingSpouse = spouseMap.get(sibling);
|
||||
if (!siblingSpouse || Math.abs(this.node(siblingSpouse).y - personNode.y) > nodeHeight) {
|
||||
const newPos = findFreePosition(personNode.x, personNode.y, sibling);
|
||||
siblingNode.x = newPos.x;
|
||||
siblingNode.y = newPos.y;
|
||||
}
|
||||
|
||||
processedSiblings.add(sibling);
|
||||
});
|
||||
|
||||
processedSiblings.add(person);
|
||||
});
|
||||
|
||||
// Create new edges
|
||||
let newEdges: Edge[] = [];
|
||||
edges.forEach((edge) => {
|
||||
let newEdge = { ...edge };
|
||||
|
||||
if (String(edge.data?.type).toLowerCase() === 'child') {
|
||||
newEdge.sourceHandle = 'child';
|
||||
newEdge.targetHandle = 'parent';
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
newEdge.hidden = false;
|
||||
newEdge.type = 'familyEdge';
|
||||
newEdges.push(newEdge);
|
||||
});
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = this.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
type: 'personNode',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { Nodes: layoutedNodes, Edges: newEdges };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user