fixup family tree layout

This commit is contained in:
2025-04-28 23:27:41 +02:00
parent b3b42bdabf
commit 8660e29ff9
6 changed files with 77 additions and 50 deletions

View File

@@ -9,43 +9,19 @@
export let id: NodeProps['id'];
export let data: NodeProps['data'] & components['schemas']['PersonProperties'];
const connection = useConnection();
let isConnecting = false;
let isTarget = false;
$: isConnecting = connection.current.fromHandle !== null;
$: isTarget = connection.current.toHandle?.id !== id;
</script>
<div
class="card card-compact bg-primary-content text-primary flex h-40 w-40 flex-col items-center justify-center rounded-full shadow-lg"
>
{#if !isConnecting}
<Handle class="customHandle" position={Position.Right} type="source" style="z-index: 1;" />
{/if}
<Handle class="customHandle" position={Position.Left} type="target" isConnectableStart={false} />
<Handle class="customHandle" isValidConnection={isValidConnection} position={Position.Bottom} type="source" style="z-index: 1;" />
<Handle class="customHandle" isValidConnection={isValidConnection} position={Position.Top} type="target" isConnectableStart={false} />
<div class="avatar mb-2">
{#if isConnecting && isTarget}
<Handle
{isValidConnection}
position={Position.Left}
type="target"
isConnectableStart={false}
style="z-index: 1;"
/>
{/if}
<div
class="ring-accent ring-offset-accent bg-accent w-24 rounded-full border-0 ring ring-offset-1"
>
{#if isConnecting && isTarget}
<Handle
{isValidConnection}
position={Position.Left}
type="target"
isConnectableStart={false}
style="z-index: 1;"
/>
{/if}
<img
src={data.profile_picture || 'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'}
alt="Picture of {data.last_name} {data.first_name}"

View File

@@ -3,7 +3,6 @@ import type { Layout } from '$lib/graph/model';
import type { Edge, Node } from '@xyflow/svelte';
export function parseFamilyTree(data: components['schemas']['FamilyTree']): Layout {
if (
data === null ||
data?.people === null ||
@@ -25,12 +24,13 @@ export function parseFamilyTree(data: components['schemas']['FamilyTree']): Layo
let relationships: Edge[] = [];
if (data.relationships) {
relationships = data.relationships.map((relationship) => {
let newEdge = { data: { ...relationship.properties } } as Edge;
if (relationship.start !== null && relationship.start !== undefined) {
newEdge.source = relationship.start.toString();
let newEdge = { data: { ...relationship.Props } } as Edge;
newEdge.data!.type = relationship.Type?.toLowerCase();
if (relationship.StartElementId !== null && relationship.StartElementId !== undefined) {
newEdge.source = relationship.StartElementId;
}
if (relationship.end !== null && relationship.end !== undefined) {
newEdge.target = relationship.end.toString();
if (relationship.EndElementId !== null && relationship.EndElementId !== undefined) {
newEdge.target = relationship.EndElementId;
}
return newEdge;

View File

@@ -2,7 +2,6 @@ import dagre from '@dagrejs/dagre';
import type { Layout } from './model';
import type { Edge, Node } from '@xyflow/svelte';
import { Position } from '@xyflow/svelte';
import PersonNode from './PersonNode.svelte';
export class FamilyTree extends dagre.graphlib.Graph {
constructor() {
@@ -18,27 +17,25 @@ export class FamilyTree extends dagre.graphlib.Graph {
): Layout {
const isHorizontal = direction === 'LR';
this.setGraph({ rankdir: direction });
this.setDefaultEdgeLabel(() => ({}))
nodes.forEach((node) => {
this.setNode(node.id, { width: nodeWidth, height: nodeHeight});
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
if (edge.data?.type === 'child') {
if (edge.data!.type === 'child') {
this.setEdge(edge.source, edge.target);
}
});
dagre.layout(this);
let newEdges: Edge[] = [];
edges.forEach((edge) => {
if (edge.data?.type === 'parent' || edge.data?.type === 'sibling') {
this.setEdge(edge.source, edge.target);
}
});
edges.forEach((edge) => {
let newEdge = edge;
if (edge.data?.type === 'spouse') {
newEdge.style = "dashed; stroke: #000; stroke-width: 2px; color: red;";
const sourceNode = this.node(edge.source);
const targetNode = this.node(edge.target);
@@ -80,6 +77,10 @@ export class FamilyTree extends dagre.graphlib.Graph {
targetNode.x = desiredX;
targetNode.y = sourceNode.y;
}
newEdge.type = 'smoothstep'
newEdges.push(newEdge);
});
const layoutedNodes = nodes.map((node) => {
@@ -91,7 +92,7 @@ export class FamilyTree extends dagre.graphlib.Graph {
// so it matches the React Flow node anchor point (top left).
return {
...node,
type: 'personNode',
type: 'personNode',
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2

View File

@@ -1,5 +1,5 @@
import type { Node, Edge, NodeTypes } from '@xyflow/svelte';
import PersonNode from './PersonNode.svelte';
import PersonNode from './PersonNode.svelte';
export const nodeTypes: NodeTypes = { personNode: PersonNode };

View File

@@ -3,6 +3,7 @@ import { parseFamilyTree } from '$lib/graph/fetch_family_tree';
import type { components } from '$lib/api/api.gen';
import type { RequestEvent } from './$types';
import { browser } from '$app/environment';
import type { Layout } from '$lib/graph/model';
export async function load(event: RequestEvent) {
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
@@ -24,7 +25,8 @@ export async function load(event: RequestEvent) {
const data = (await response.json()) as components['schemas']['FamilyTree'];
let layout = parseFamilyTree(data)
let layout = parseFamilyTree(data) as Layout & {id: string};
layout.id = event.locals.session.userId;
return layout;
}

View File

@@ -26,7 +26,7 @@
import { FamilyTree } from '$lib/graph/layout';
import { tailwindClassToPixels } from '$lib/tailwindSizeToPx';
import type { Layout } from '$lib/graph/model';
let { data }: { data: Layout } = $props();
let { data }: { data: Layout & { id: string } } = $props();
let selectedPerson: components['schemas']['PersonProperties'] & { id: number | null } = $state({
id: null
@@ -66,7 +66,29 @@
openPersonMenu = undefined;
},
deleteNode: () => {
relationshipStart = Number(node.id);
if (Number(data.id) === Number(node.id)) {
relationshipStart = null;
openPersonMenu = undefined;
return;
}
fetch('/api/person/' + node.id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
nodes = nodes.filter((n) => n.id !== node.id);
edges = edges.filter((e) => e.source !== node.id && e.target !== node.id);
} else {
alert('Error deleting person');
}
})
.catch((error) => {
console.error('Error:', error);
});
openPersonMenu = undefined;
},
createRelationshipAndNode: () => {
@@ -102,13 +124,16 @@
edges = [...edges, ...newEdges];
}
familyTreeDAG.getLayoutedElements(
let newLayout = familyTreeDAG.getLayoutedElements(
nodes,
edges,
tailwindClassToPixels('w-40') || 160,
tailwindClassToPixels('h-40') || 160,
'TB'
);
edges = newLayout.Edges;
nodes = newLayout.Nodes;
};
let handleNodeClickFunc = handleNodeClick(
@@ -118,8 +143,28 @@
}
) => {
openPersonPanel = true;
console.log('person', person);
selectedPerson = person;
fetch('/api/person/' + person.id, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
return response.json() as Promise<components['schemas']['Person']>;
} else {
alert('Error fetching person data');
return null;
}
})
.then((data) => {
if (data) {
selectedPerson = data.Props as components['schemas']['PersonProperties'] & {
id: number | null;
};
}
});
}
);
@@ -161,6 +206,9 @@
{/if}
{#if createPerson}
<CreatePerson
onOnlyPersonCreation={() => {
createPerson = false;
}}
{onCreation}
closeModal={() => {
createPerson = false;