@famtree/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +9 -0
- package/dist/graph.d.ts +22 -0
- package/dist/index.cjs +238 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +34 -0
- package/package.json +34 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const NODE = 46;
|
|
2
|
+
declare const R: number;
|
|
3
|
+
declare const GEN_SPACING = 150;
|
|
4
|
+
declare const SIB_STEP = 128;
|
|
5
|
+
declare const SPOUSE_HALF = 64;
|
|
6
|
+
declare const MARGIN = 70;
|
|
7
|
+
declare const LABEL_WRAP = 16;
|
|
8
|
+
declare const LABEL_LINE_H = 13;
|
|
9
|
+
export { NODE, R, GEN_SPACING, SIB_STEP, SPOUSE_HALF, MARGIN, LABEL_WRAP, LABEL_LINE_H };
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Genogram, Person, UnionRelationship, ParentChildRelationship, EmotionalRelationship } from "@famtree/schema";
|
|
2
|
+
declare class GenogramGraph {
|
|
3
|
+
readonly doc: Genogram;
|
|
4
|
+
readonly people: Map<string, Person>;
|
|
5
|
+
readonly unions: UnionRelationship[];
|
|
6
|
+
readonly parentChild: ParentChildRelationship[];
|
|
7
|
+
readonly emotions: EmotionalRelationship[];
|
|
8
|
+
private readonly unionById;
|
|
9
|
+
private readonly pcByChild;
|
|
10
|
+
private readonly gen;
|
|
11
|
+
constructor(doc: Genogram);
|
|
12
|
+
get roster(): Person[];
|
|
13
|
+
parentLinkOf(id: string): ParentChildRelationship | undefined;
|
|
14
|
+
parentsOf(id: string): string[];
|
|
15
|
+
unionHeadedBy(id: string): UnionRelationship | undefined;
|
|
16
|
+
childrenOfUnion(uid: string): string[];
|
|
17
|
+
singleChildrenOf(id: string): string[];
|
|
18
|
+
sortByBirth(ids: string[]): string[];
|
|
19
|
+
generationOf(id: string): number;
|
|
20
|
+
private computeGenerations;
|
|
21
|
+
}
|
|
22
|
+
export { GenogramGraph };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
GEN_SPACING: () => GEN_SPACING,
|
|
24
|
+
GenerationalLayout: () => GenerationalLayout,
|
|
25
|
+
GenogramGraph: () => GenogramGraph,
|
|
26
|
+
LABEL_LINE_H: () => LABEL_LINE_H,
|
|
27
|
+
LABEL_WRAP: () => LABEL_WRAP,
|
|
28
|
+
MARGIN: () => MARGIN,
|
|
29
|
+
NODE: () => NODE,
|
|
30
|
+
Positions: () => Positions,
|
|
31
|
+
R: () => R,
|
|
32
|
+
SIB_STEP: () => SIB_STEP,
|
|
33
|
+
SPOUSE_HALF: () => SPOUSE_HALF
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
|
+
|
|
37
|
+
// src/graph.ts
|
|
38
|
+
var isUnion = (r) => r.type === "union";
|
|
39
|
+
var isParentChild = (r) => r.type === "parent-child";
|
|
40
|
+
var isEmotional = (r) => r.type === "emotional";
|
|
41
|
+
var GenogramGraph = class {
|
|
42
|
+
constructor(doc) {
|
|
43
|
+
this.doc = doc;
|
|
44
|
+
this.people = new Map(doc.people.map((p) => [p.id, p]));
|
|
45
|
+
this.unions = doc.relationships.filter(isUnion);
|
|
46
|
+
this.parentChild = doc.relationships.filter(isParentChild);
|
|
47
|
+
this.emotions = doc.relationships.filter(isEmotional);
|
|
48
|
+
this.unionById = new Map(this.unions.map((u) => [u.id, u]));
|
|
49
|
+
this.pcByChild = new Map(this.parentChild.map((pc) => [pc.childId, pc]));
|
|
50
|
+
this.computeGenerations();
|
|
51
|
+
}
|
|
52
|
+
doc;
|
|
53
|
+
people;
|
|
54
|
+
unions;
|
|
55
|
+
parentChild;
|
|
56
|
+
emotions;
|
|
57
|
+
unionById;
|
|
58
|
+
pcByChild;
|
|
59
|
+
gen = /* @__PURE__ */ new Map();
|
|
60
|
+
get roster() {
|
|
61
|
+
return this.doc.people;
|
|
62
|
+
}
|
|
63
|
+
parentLinkOf(id) {
|
|
64
|
+
return this.pcByChild.get(id);
|
|
65
|
+
}
|
|
66
|
+
parentsOf(id) {
|
|
67
|
+
const pc = this.pcByChild.get(id);
|
|
68
|
+
if (!pc) return [];
|
|
69
|
+
if (pc.unionId) return this.unionById.get(pc.unionId)?.partnerIds ?? [];
|
|
70
|
+
return pc.parentIds ?? [];
|
|
71
|
+
}
|
|
72
|
+
unionHeadedBy(id) {
|
|
73
|
+
return this.unions.find((u) => u.partnerIds.includes(id));
|
|
74
|
+
}
|
|
75
|
+
childrenOfUnion(uid) {
|
|
76
|
+
return this.parentChild.filter((pc) => pc.unionId === uid).map((pc) => pc.childId);
|
|
77
|
+
}
|
|
78
|
+
singleChildrenOf(id) {
|
|
79
|
+
return this.parentChild.filter((pc) => !pc.unionId && (pc.parentIds ?? []).includes(id)).map((pc) => pc.childId);
|
|
80
|
+
}
|
|
81
|
+
sortByBirth(ids) {
|
|
82
|
+
return [...ids].sort((a, b) => {
|
|
83
|
+
const pa = this.people.get(a), pb = this.people.get(b);
|
|
84
|
+
return (pa.birthOrder ?? 99) - (pb.birthOrder ?? 99) || (pa.birthDate ?? "").localeCompare(pb.birthDate ?? "");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
generationOf(id) {
|
|
88
|
+
return this.gen.get(id) ?? 0;
|
|
89
|
+
}
|
|
90
|
+
// Assign each person a generation index, then align partners to the deeper
|
|
91
|
+
// generation and push descendants below their parents until stable.
|
|
92
|
+
computeGenerations() {
|
|
93
|
+
const visit = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
94
|
+
if (this.gen.has(id)) return this.gen.get(id);
|
|
95
|
+
if (seen.has(id)) return 0;
|
|
96
|
+
seen.add(id);
|
|
97
|
+
const ps = this.parentsOf(id);
|
|
98
|
+
const val = ps.length ? Math.max(...ps.map((p) => visit(p, seen))) + 1 : 0;
|
|
99
|
+
this.gen.set(id, val);
|
|
100
|
+
return val;
|
|
101
|
+
};
|
|
102
|
+
for (const p of this.doc.people) visit(p.id);
|
|
103
|
+
for (let pass = 0; pass < this.doc.people.length; pass++) {
|
|
104
|
+
let changed = false;
|
|
105
|
+
for (const u of this.unions) {
|
|
106
|
+
const [a, b] = u.partnerIds;
|
|
107
|
+
const m = Math.max(this.gen.get(a), this.gen.get(b));
|
|
108
|
+
if (this.gen.get(a) !== m || this.gen.get(b) !== m) {
|
|
109
|
+
this.gen.set(a, m);
|
|
110
|
+
this.gen.set(b, m);
|
|
111
|
+
changed = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const pc of this.parentChild) {
|
|
115
|
+
const parents = this.parentsOf(pc.childId);
|
|
116
|
+
const pg = Math.max(0, ...parents.map((p) => this.gen.get(p)));
|
|
117
|
+
if (parents.length && this.gen.get(pc.childId) <= pg) {
|
|
118
|
+
this.gen.set(pc.childId, pg + 1);
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!changed) break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/constants.ts
|
|
128
|
+
var NODE = 46;
|
|
129
|
+
var R = NODE / 2;
|
|
130
|
+
var GEN_SPACING = 150;
|
|
131
|
+
var SIB_STEP = 128;
|
|
132
|
+
var SPOUSE_HALF = 64;
|
|
133
|
+
var MARGIN = 70;
|
|
134
|
+
var LABEL_WRAP = 16;
|
|
135
|
+
var LABEL_LINE_H = 13;
|
|
136
|
+
|
|
137
|
+
// src/layout.ts
|
|
138
|
+
var Positions = class {
|
|
139
|
+
constructor(graph, xOf) {
|
|
140
|
+
this.graph = graph;
|
|
141
|
+
this.xOf = xOf;
|
|
142
|
+
}
|
|
143
|
+
graph;
|
|
144
|
+
xOf;
|
|
145
|
+
cx(id) {
|
|
146
|
+
return this.xOf.get(id) + MARGIN + R;
|
|
147
|
+
}
|
|
148
|
+
cy(id) {
|
|
149
|
+
return this.graph.generationOf(id) * GEN_SPACING + MARGIN + R;
|
|
150
|
+
}
|
|
151
|
+
bounds() {
|
|
152
|
+
const ids = this.graph.roster.map((p) => p.id);
|
|
153
|
+
return {
|
|
154
|
+
width: Math.max(...ids.map((id) => this.cx(id))) + MARGIN + 60,
|
|
155
|
+
height: Math.max(...ids.map((id) => this.cy(id))) + MARGIN + 60
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var GenerationalLayout = class {
|
|
160
|
+
graph;
|
|
161
|
+
xOf;
|
|
162
|
+
placedUnion;
|
|
163
|
+
cursor = 0;
|
|
164
|
+
layout(graph) {
|
|
165
|
+
this.graph = graph;
|
|
166
|
+
this.xOf = /* @__PURE__ */ new Map();
|
|
167
|
+
this.placedUnion = /* @__PURE__ */ new Set();
|
|
168
|
+
this.cursor = 0;
|
|
169
|
+
const roots = graph.roster.map((p) => p.id).filter((id) => graph.parentsOf(id).length === 0).sort((a, b) => (graph.unionHeadedBy(a) ? 0 : 1) - (graph.unionHeadedBy(b) ? 0 : 1));
|
|
170
|
+
for (const id of roots) this.placeSubtree(id);
|
|
171
|
+
for (const p of graph.roster) if (!this.xOf.has(p.id)) this.placeSubtree(p.id);
|
|
172
|
+
for (const pos of graph.doc.layout?.positions ?? []) {
|
|
173
|
+
this.xOf.set(pos.personId, pos.x);
|
|
174
|
+
}
|
|
175
|
+
return new Positions(graph, this.xOf);
|
|
176
|
+
}
|
|
177
|
+
placeSubtree(id) {
|
|
178
|
+
const existing = this.xOf.get(id);
|
|
179
|
+
if (existing !== void 0) return existing;
|
|
180
|
+
const union = this.graph.unionHeadedBy(id);
|
|
181
|
+
if (union && !this.placedUnion.has(union.id)) return this.placeUnion(union, id);
|
|
182
|
+
const kids = this.orderSiblings(this.graph.singleChildrenOf(id));
|
|
183
|
+
if (kids.length) {
|
|
184
|
+
const x2 = this.centerOver(kids);
|
|
185
|
+
this.xOf.set(id, x2);
|
|
186
|
+
return x2;
|
|
187
|
+
}
|
|
188
|
+
const x = this.cursor;
|
|
189
|
+
this.cursor += SIB_STEP;
|
|
190
|
+
this.xOf.set(id, x);
|
|
191
|
+
return x;
|
|
192
|
+
}
|
|
193
|
+
placeUnion(union, anchor) {
|
|
194
|
+
this.placedUnion.add(union.id);
|
|
195
|
+
const other = union.partnerIds.find((p) => p !== anchor);
|
|
196
|
+
const kids = this.orderSiblings(this.graph.childrenOfUnion(union.id));
|
|
197
|
+
let center;
|
|
198
|
+
if (kids.length) {
|
|
199
|
+
center = this.centerOver(kids);
|
|
200
|
+
} else {
|
|
201
|
+
center = this.cursor + SIB_STEP / 2;
|
|
202
|
+
this.cursor += SIB_STEP * 2;
|
|
203
|
+
}
|
|
204
|
+
this.xOf.set(anchor, center - SPOUSE_HALF);
|
|
205
|
+
this.xOf.set(other, center + SPOUSE_HALF);
|
|
206
|
+
this.cursor = Math.max(this.cursor, center + SPOUSE_HALF + SIB_STEP);
|
|
207
|
+
return center;
|
|
208
|
+
}
|
|
209
|
+
centerOver(ids) {
|
|
210
|
+
const xs = ids.map((k) => this.placeSubtree(k));
|
|
211
|
+
return (xs[0] + xs[xs.length - 1]) / 2;
|
|
212
|
+
}
|
|
213
|
+
// Birth order, but children who themselves head a union (bringing in a
|
|
214
|
+
// married-in spouse and that spouse's own cluster) are placed last so the
|
|
215
|
+
// horizontal expansion happens at the group's edge instead of splitting the
|
|
216
|
+
// blood-sibling row.
|
|
217
|
+
orderSiblings(ids) {
|
|
218
|
+
const sorted = this.graph.sortByBirth(ids);
|
|
219
|
+
const singles = sorted.filter((id) => !this.graph.unionHeadedBy(id));
|
|
220
|
+
const married = sorted.filter((id) => this.graph.unionHeadedBy(id));
|
|
221
|
+
return [...singles, ...married];
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
225
|
+
0 && (module.exports = {
|
|
226
|
+
GEN_SPACING,
|
|
227
|
+
GenerationalLayout,
|
|
228
|
+
GenogramGraph,
|
|
229
|
+
LABEL_LINE_H,
|
|
230
|
+
LABEL_WRAP,
|
|
231
|
+
MARGIN,
|
|
232
|
+
NODE,
|
|
233
|
+
Positions,
|
|
234
|
+
R,
|
|
235
|
+
SIB_STEP,
|
|
236
|
+
SPOUSE_HALF
|
|
237
|
+
});
|
|
238
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/graph.ts","../src/constants.ts","../src/layout.ts"],"sourcesContent":["export { GenogramGraph } from \"./graph\"\nexport { GenerationalLayout, Positions } from \"./layout\"\nexport type { LayoutEngine } from \"./layout\"\nexport {\n NODE,\n R,\n GEN_SPACING,\n SIB_STEP,\n SPOUSE_HALF,\n MARGIN,\n LABEL_WRAP,\n LABEL_LINE_H,\n} from \"./constants\"\n","// GenogramGraph: an indexed, query-friendly view over a raw genogram document.\n// Owns the family-structure domain logic (who are the parents, children, unions)\n// and the generation (vertical rank) computation. Rendering lives elsewhere.\n\nimport type {\n Genogram,\n Person,\n Relationship,\n UnionRelationship,\n ParentChildRelationship,\n EmotionalRelationship,\n} from \"@famtree/schema\"\n\nconst isUnion = (r: Relationship): r is UnionRelationship => r.type === \"union\"\nconst isParentChild = (r: Relationship): r is ParentChildRelationship => r.type === \"parent-child\"\nconst isEmotional = (r: Relationship): r is EmotionalRelationship => r.type === \"emotional\"\n\nclass GenogramGraph {\n readonly people: Map<string, Person>\n readonly unions: UnionRelationship[]\n readonly parentChild: ParentChildRelationship[]\n readonly emotions: EmotionalRelationship[]\n\n private readonly unionById: Map<string, UnionRelationship>\n private readonly pcByChild: Map<string, ParentChildRelationship>\n private readonly gen = new Map<string, number>()\n\n constructor(readonly doc: Genogram) {\n this.people = new Map(doc.people.map((p) => [p.id, p]))\n this.unions = doc.relationships.filter(isUnion)\n this.parentChild = doc.relationships.filter(isParentChild)\n this.emotions = doc.relationships.filter(isEmotional)\n this.unionById = new Map(this.unions.map((u) => [u.id, u]))\n this.pcByChild = new Map(this.parentChild.map((pc) => [pc.childId, pc]))\n this.computeGenerations()\n }\n\n get roster(): Person[] {\n return this.doc.people\n }\n\n parentLinkOf(id: string): ParentChildRelationship | undefined {\n return this.pcByChild.get(id)\n }\n\n parentsOf(id: string): string[] {\n const pc = this.pcByChild.get(id)\n if (!pc) return []\n if (pc.unionId) return this.unionById.get(pc.unionId)?.partnerIds ?? []\n return pc.parentIds ?? []\n }\n\n unionHeadedBy(id: string): UnionRelationship | undefined {\n return this.unions.find((u) => u.partnerIds.includes(id))\n }\n\n childrenOfUnion(uid: string): string[] {\n return this.parentChild.filter((pc) => pc.unionId === uid).map((pc) => pc.childId)\n }\n\n singleChildrenOf(id: string): string[] {\n return this.parentChild.filter((pc) => !pc.unionId && (pc.parentIds ?? []).includes(id)).map((pc) => pc.childId)\n }\n\n sortByBirth(ids: string[]): string[] {\n return [...ids].sort((a, b) => {\n const pa = this.people.get(a)!,\n pb = this.people.get(b)!\n return (pa.birthOrder ?? 99) - (pb.birthOrder ?? 99) || (pa.birthDate ?? \"\").localeCompare(pb.birthDate ?? \"\")\n })\n }\n\n generationOf(id: string): number {\n return this.gen.get(id) ?? 0\n }\n\n // Assign each person a generation index, then align partners to the deeper\n // generation and push descendants below their parents until stable.\n private computeGenerations(): void {\n const visit = (id: string, seen = new Set<string>()): number => {\n if (this.gen.has(id)) return this.gen.get(id)!\n if (seen.has(id)) return 0\n seen.add(id)\n const ps = this.parentsOf(id)\n const val = ps.length ? Math.max(...ps.map((p) => visit(p, seen))) + 1 : 0\n this.gen.set(id, val)\n return val\n }\n for (const p of this.doc.people) visit(p.id)\n\n for (let pass = 0; pass < this.doc.people.length; pass++) {\n let changed = false\n for (const u of this.unions) {\n const [a, b] = u.partnerIds\n const m = Math.max(this.gen.get(a)!, this.gen.get(b)!)\n if (this.gen.get(a) !== m || this.gen.get(b) !== m) {\n this.gen.set(a, m)\n this.gen.set(b, m)\n changed = true\n }\n }\n for (const pc of this.parentChild) {\n const parents = this.parentsOf(pc.childId)\n const pg = Math.max(0, ...parents.map((p) => this.gen.get(p)!))\n if (parents.length && this.gen.get(pc.childId)! <= pg) {\n this.gen.set(pc.childId, pg + 1)\n changed = true\n }\n }\n if (!changed) break\n }\n }\n}\n\nexport { GenogramGraph }\n","// Shared geometry constants for layout and rendering.\nconst NODE = 46 // node width / diameter\nconst R = NODE / 2 // node radius\nconst GEN_SPACING = 150 // vertical distance between generations\nconst SIB_STEP = 128 // horizontal step between sibling subtrees\nconst SPOUSE_HALF = 64 // half distance between partners in a couple\nconst MARGIN = 70 // outer margin / frame padding\nconst LABEL_WRAP = 16 // max characters per label line before wrapping\nconst LABEL_LINE_H = 13 // vertical distance between wrapped label lines\n\nexport { NODE, R, GEN_SPACING, SIB_STEP, SPOUSE_HALF, MARGIN, LABEL_WRAP, LABEL_LINE_H }\n","// Layout: turns a GenogramGraph into concrete x/y coordinates.\n// `LayoutEngine` is an abstraction so the renderer can depend on it rather than a\n// concrete algorithm (Dependency Inversion / Open-Closed for new strategies).\n\nimport type { GenogramGraph } from \"./graph\"\nimport type { UnionRelationship } from \"@famtree/schema\"\nimport { GEN_SPACING, MARGIN, R, SIB_STEP, SPOUSE_HALF } from \"./constants\"\n\n/** Resolved coordinates for a laid-out genogram. */\nclass Positions {\n constructor(\n private readonly graph: GenogramGraph,\n private readonly xOf: Map<string, number>,\n ) {}\n\n cx(id: string): number {\n return this.xOf.get(id)! + MARGIN + R\n }\n\n cy(id: string): number {\n return this.graph.generationOf(id) * GEN_SPACING + MARGIN + R\n }\n\n bounds(): { width: number; height: number } {\n const ids = this.graph.roster.map((p) => p.id)\n return {\n width: Math.max(...ids.map((id) => this.cx(id))) + MARGIN + 60,\n height: Math.max(...ids.map((id) => this.cy(id))) + MARGIN + 60,\n }\n }\n}\n\ninterface LayoutEngine {\n layout(graph: GenogramGraph): Positions\n}\n\n/**\n * Places couples centered above their children, oldest sibling to the left,\n * married-in spouses to the right of the blood relative. Honors explicit\n * `layout.positions` overrides from the document.\n */\nclass GenerationalLayout implements LayoutEngine {\n private graph!: GenogramGraph\n private xOf!: Map<string, number>\n private placedUnion!: Set<string>\n private cursor = 0\n\n layout(graph: GenogramGraph): Positions {\n this.graph = graph\n this.xOf = new Map()\n this.placedUnion = new Set()\n this.cursor = 0\n\n // Place ancestors (people with no parents) first; couples before singles.\n const roots = graph.roster\n .map((p) => p.id)\n .filter((id) => graph.parentsOf(id).length === 0)\n .sort((a, b) => (graph.unionHeadedBy(a) ? 0 : 1) - (graph.unionHeadedBy(b) ? 0 : 1))\n for (const id of roots) this.placeSubtree(id)\n for (const p of graph.roster) if (!this.xOf.has(p.id)) this.placeSubtree(p.id)\n\n for (const pos of graph.doc.layout?.positions ?? []) {\n this.xOf.set(pos.personId, pos.x)\n }\n return new Positions(graph, this.xOf)\n }\n\n private placeSubtree(id: string): number {\n const existing = this.xOf.get(id)\n if (existing !== undefined) return existing\n\n const union = this.graph.unionHeadedBy(id)\n if (union && !this.placedUnion.has(union.id)) return this.placeUnion(union, id)\n\n const kids = this.orderSiblings(this.graph.singleChildrenOf(id))\n if (kids.length) {\n const x = this.centerOver(kids)\n this.xOf.set(id, x)\n return x\n }\n const x = this.cursor\n this.cursor += SIB_STEP\n this.xOf.set(id, x)\n return x\n }\n\n private placeUnion(union: UnionRelationship, anchor: string): number {\n this.placedUnion.add(union.id)\n const other = union.partnerIds.find((p) => p !== anchor)!\n const kids = this.orderSiblings(this.graph.childrenOfUnion(union.id))\n\n let center: number\n if (kids.length) {\n center = this.centerOver(kids)\n } else {\n center = this.cursor + SIB_STEP / 2\n this.cursor += SIB_STEP * 2\n }\n this.xOf.set(anchor, center - SPOUSE_HALF)\n this.xOf.set(other, center + SPOUSE_HALF)\n this.cursor = Math.max(this.cursor, center + SPOUSE_HALF + SIB_STEP)\n return center\n }\n\n private centerOver(ids: string[]): number {\n const xs = ids.map((k) => this.placeSubtree(k))\n return (xs[0] + xs[xs.length - 1]) / 2\n }\n\n // Birth order, but children who themselves head a union (bringing in a\n // married-in spouse and that spouse's own cluster) are placed last so the\n // horizontal expansion happens at the group's edge instead of splitting the\n // blood-sibling row.\n private orderSiblings(ids: string[]): string[] {\n const sorted = this.graph.sortByBirth(ids)\n const singles = sorted.filter((id) => !this.graph.unionHeadedBy(id))\n const married = sorted.filter((id) => this.graph.unionHeadedBy(id))\n return [...singles, ...married]\n }\n}\n\nexport { Positions, GenerationalLayout }\nexport type { LayoutEngine }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACaA,IAAM,UAAU,CAAC,MAA4C,EAAE,SAAS;AACxE,IAAM,gBAAgB,CAAC,MAAkD,EAAE,SAAS;AACpF,IAAM,cAAc,CAAC,MAAgD,EAAE,SAAS;AAEhF,IAAM,gBAAN,MAAoB;AAAA,EAUlB,YAAqB,KAAe;AAAf;AACnB,SAAK,SAAS,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACtD,SAAK,SAAS,IAAI,cAAc,OAAO,OAAO;AAC9C,SAAK,cAAc,IAAI,cAAc,OAAO,aAAa;AACzD,SAAK,WAAW,IAAI,cAAc,OAAO,WAAW;AACpD,SAAK,YAAY,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAC1D,SAAK,YAAY,IAAI,IAAI,KAAK,YAAY,IAAI,CAAC,OAAO,CAAC,GAAG,SAAS,EAAE,CAAC,CAAC;AACvE,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EARqB;AAAA,EATZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA,MAAM,oBAAI,IAAoB;AAAA,EAY/C,IAAI,SAAmB;AACrB,WAAO,KAAK,IAAI;AAAA,EAClB;AAAA,EAEA,aAAa,IAAiD;AAC5D,WAAO,KAAK,UAAU,IAAI,EAAE;AAAA,EAC9B;AAAA,EAEA,UAAU,IAAsB;AAC9B,UAAM,KAAK,KAAK,UAAU,IAAI,EAAE;AAChC,QAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAI,GAAG,QAAS,QAAO,KAAK,UAAU,IAAI,GAAG,OAAO,GAAG,cAAc,CAAC;AACtE,WAAO,GAAG,aAAa,CAAC;AAAA,EAC1B;AAAA,EAEA,cAAc,IAA2C;AACvD,WAAO,KAAK,OAAO,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,EAAE,CAAC;AAAA,EAC1D;AAAA,EAEA,gBAAgB,KAAuB;AACrC,WAAO,KAAK,YAAY,OAAO,CAAC,OAAO,GAAG,YAAY,GAAG,EAAE,IAAI,CAAC,OAAO,GAAG,OAAO;AAAA,EACnF;AAAA,EAEA,iBAAiB,IAAsB;AACrC,WAAO,KAAK,YAAY,OAAO,CAAC,OAAO,CAAC,GAAG,YAAY,GAAG,aAAa,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,GAAG,OAAO;AAAA,EACjH;AAAA,EAEA,YAAY,KAAyB;AACnC,WAAO,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM;AAC7B,YAAM,KAAK,KAAK,OAAO,IAAI,CAAC,GAC1B,KAAK,KAAK,OAAO,IAAI,CAAC;AACxB,cAAQ,GAAG,cAAc,OAAO,GAAG,cAAc,QAAQ,GAAG,aAAa,IAAI,cAAc,GAAG,aAAa,EAAE;AAAA,IAC/G,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,IAAoB;AAC/B,WAAO,KAAK,IAAI,IAAI,EAAE,KAAK;AAAA,EAC7B;AAAA;AAAA;AAAA,EAIQ,qBAA2B;AACjC,UAAM,QAAQ,CAAC,IAAY,OAAO,oBAAI,IAAY,MAAc;AAC9D,UAAI,KAAK,IAAI,IAAI,EAAE,EAAG,QAAO,KAAK,IAAI,IAAI,EAAE;AAC5C,UAAI,KAAK,IAAI,EAAE,EAAG,QAAO;AACzB,WAAK,IAAI,EAAE;AACX,YAAM,KAAK,KAAK,UAAU,EAAE;AAC5B,YAAM,MAAM,GAAG,SAAS,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI;AACzE,WAAK,IAAI,IAAI,IAAI,GAAG;AACpB,aAAO;AAAA,IACT;AACA,eAAW,KAAK,KAAK,IAAI,OAAQ,OAAM,EAAE,EAAE;AAE3C,aAAS,OAAO,GAAG,OAAO,KAAK,IAAI,OAAO,QAAQ,QAAQ;AACxD,UAAI,UAAU;AACd,iBAAW,KAAK,KAAK,QAAQ;AAC3B,cAAM,CAAC,GAAG,CAAC,IAAI,EAAE;AACjB,cAAM,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,GAAI,KAAK,IAAI,IAAI,CAAC,CAAE;AACrD,YAAI,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,GAAG;AAClD,eAAK,IAAI,IAAI,GAAG,CAAC;AACjB,eAAK,IAAI,IAAI,GAAG,CAAC;AACjB,oBAAU;AAAA,QACZ;AAAA,MACF;AACA,iBAAW,MAAM,KAAK,aAAa;AACjC,cAAM,UAAU,KAAK,UAAU,GAAG,OAAO;AACzC,cAAM,KAAK,KAAK,IAAI,GAAG,GAAG,QAAQ,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAE,CAAC;AAC9D,YAAI,QAAQ,UAAU,KAAK,IAAI,IAAI,GAAG,OAAO,KAAM,IAAI;AACrD,eAAK,IAAI,IAAI,GAAG,SAAS,KAAK,CAAC;AAC/B,oBAAU;AAAA,QACZ;AAAA,MACF;AACA,UAAI,CAAC,QAAS;AAAA,IAChB;AAAA,EACF;AACF;;;AC/GA,IAAM,OAAO;AACb,IAAM,IAAI,OAAO;AACjB,IAAM,cAAc;AACpB,IAAM,WAAW;AACjB,IAAM,cAAc;AACpB,IAAM,SAAS;AACf,IAAM,aAAa;AACnB,IAAM,eAAe;;;ACCrB,IAAM,YAAN,MAAgB;AAAA,EACd,YACmB,OACA,KACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAGnB,GAAG,IAAoB;AACrB,WAAO,KAAK,IAAI,IAAI,EAAE,IAAK,SAAS;AAAA,EACtC;AAAA,EAEA,GAAG,IAAoB;AACrB,WAAO,KAAK,MAAM,aAAa,EAAE,IAAI,cAAc,SAAS;AAAA,EAC9D;AAAA,EAEA,SAA4C;AAC1C,UAAM,MAAM,KAAK,MAAM,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE;AAC7C,WAAO;AAAA,MACL,OAAO,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC,CAAC,IAAI,SAAS;AAAA,MAC5D,QAAQ,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC,CAAC,IAAI,SAAS;AAAA,IAC/D;AAAA,EACF;AACF;AAWA,IAAM,qBAAN,MAAiD;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EAEjB,OAAO,OAAiC;AACtC,SAAK,QAAQ;AACb,SAAK,MAAM,oBAAI,IAAI;AACnB,SAAK,cAAc,oBAAI,IAAI;AAC3B,SAAK,SAAS;AAGd,UAAM,QAAQ,MAAM,OACjB,IAAI,CAAC,MAAM,EAAE,EAAE,EACf,OAAO,CAAC,OAAO,MAAM,UAAU,EAAE,EAAE,WAAW,CAAC,EAC/C,KAAK,CAAC,GAAG,OAAO,MAAM,cAAc,CAAC,IAAI,IAAI,MAAM,MAAM,cAAc,CAAC,IAAI,IAAI,EAAE;AACrF,eAAW,MAAM,MAAO,MAAK,aAAa,EAAE;AAC5C,eAAW,KAAK,MAAM,OAAQ,KAAI,CAAC,KAAK,IAAI,IAAI,EAAE,EAAE,EAAG,MAAK,aAAa,EAAE,EAAE;AAE7E,eAAW,OAAO,MAAM,IAAI,QAAQ,aAAa,CAAC,GAAG;AACnD,WAAK,IAAI,IAAI,IAAI,UAAU,IAAI,CAAC;AAAA,IAClC;AACA,WAAO,IAAI,UAAU,OAAO,KAAK,GAAG;AAAA,EACtC;AAAA,EAEQ,aAAa,IAAoB;AACvC,UAAM,WAAW,KAAK,IAAI,IAAI,EAAE;AAChC,QAAI,aAAa,OAAW,QAAO;AAEnC,UAAM,QAAQ,KAAK,MAAM,cAAc,EAAE;AACzC,QAAI,SAAS,CAAC,KAAK,YAAY,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,WAAW,OAAO,EAAE;AAE9E,UAAM,OAAO,KAAK,cAAc,KAAK,MAAM,iBAAiB,EAAE,CAAC;AAC/D,QAAI,KAAK,QAAQ;AACf,YAAMA,KAAI,KAAK,WAAW,IAAI;AAC9B,WAAK,IAAI,IAAI,IAAIA,EAAC;AAClB,aAAOA;AAAA,IACT;AACA,UAAM,IAAI,KAAK;AACf,SAAK,UAAU;AACf,SAAK,IAAI,IAAI,IAAI,CAAC;AAClB,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,OAA0B,QAAwB;AACnE,SAAK,YAAY,IAAI,MAAM,EAAE;AAC7B,UAAM,QAAQ,MAAM,WAAW,KAAK,CAAC,MAAM,MAAM,MAAM;AACvD,UAAM,OAAO,KAAK,cAAc,KAAK,MAAM,gBAAgB,MAAM,EAAE,CAAC;AAEpE,QAAI;AACJ,QAAI,KAAK,QAAQ;AACf,eAAS,KAAK,WAAW,IAAI;AAAA,IAC/B,OAAO;AACL,eAAS,KAAK,SAAS,WAAW;AAClC,WAAK,UAAU,WAAW;AAAA,IAC5B;AACA,SAAK,IAAI,IAAI,QAAQ,SAAS,WAAW;AACzC,SAAK,IAAI,IAAI,OAAO,SAAS,WAAW;AACxC,SAAK,SAAS,KAAK,IAAI,KAAK,QAAQ,SAAS,cAAc,QAAQ;AACnE,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,KAAuB;AACxC,UAAM,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;AAC9C,YAAQ,GAAG,CAAC,IAAI,GAAG,GAAG,SAAS,CAAC,KAAK;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,KAAyB;AAC7C,UAAM,SAAS,KAAK,MAAM,YAAY,GAAG;AACzC,UAAM,UAAU,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,cAAc,EAAE,CAAC;AACnE,UAAM,UAAU,OAAO,OAAO,CAAC,OAAO,KAAK,MAAM,cAAc,EAAE,CAAC;AAClE,WAAO,CAAC,GAAG,SAAS,GAAG,OAAO;AAAA,EAChC;AACF;","names":["x"]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/graph.ts
|
|
2
|
+
var isUnion = (r) => r.type === "union";
|
|
3
|
+
var isParentChild = (r) => r.type === "parent-child";
|
|
4
|
+
var isEmotional = (r) => r.type === "emotional";
|
|
5
|
+
var GenogramGraph = class {
|
|
6
|
+
constructor(doc) {
|
|
7
|
+
this.doc = doc;
|
|
8
|
+
this.people = new Map(doc.people.map((p) => [p.id, p]));
|
|
9
|
+
this.unions = doc.relationships.filter(isUnion);
|
|
10
|
+
this.parentChild = doc.relationships.filter(isParentChild);
|
|
11
|
+
this.emotions = doc.relationships.filter(isEmotional);
|
|
12
|
+
this.unionById = new Map(this.unions.map((u) => [u.id, u]));
|
|
13
|
+
this.pcByChild = new Map(this.parentChild.map((pc) => [pc.childId, pc]));
|
|
14
|
+
this.computeGenerations();
|
|
15
|
+
}
|
|
16
|
+
doc;
|
|
17
|
+
people;
|
|
18
|
+
unions;
|
|
19
|
+
parentChild;
|
|
20
|
+
emotions;
|
|
21
|
+
unionById;
|
|
22
|
+
pcByChild;
|
|
23
|
+
gen = /* @__PURE__ */ new Map();
|
|
24
|
+
get roster() {
|
|
25
|
+
return this.doc.people;
|
|
26
|
+
}
|
|
27
|
+
parentLinkOf(id) {
|
|
28
|
+
return this.pcByChild.get(id);
|
|
29
|
+
}
|
|
30
|
+
parentsOf(id) {
|
|
31
|
+
const pc = this.pcByChild.get(id);
|
|
32
|
+
if (!pc) return [];
|
|
33
|
+
if (pc.unionId) return this.unionById.get(pc.unionId)?.partnerIds ?? [];
|
|
34
|
+
return pc.parentIds ?? [];
|
|
35
|
+
}
|
|
36
|
+
unionHeadedBy(id) {
|
|
37
|
+
return this.unions.find((u) => u.partnerIds.includes(id));
|
|
38
|
+
}
|
|
39
|
+
childrenOfUnion(uid) {
|
|
40
|
+
return this.parentChild.filter((pc) => pc.unionId === uid).map((pc) => pc.childId);
|
|
41
|
+
}
|
|
42
|
+
singleChildrenOf(id) {
|
|
43
|
+
return this.parentChild.filter((pc) => !pc.unionId && (pc.parentIds ?? []).includes(id)).map((pc) => pc.childId);
|
|
44
|
+
}
|
|
45
|
+
sortByBirth(ids) {
|
|
46
|
+
return [...ids].sort((a, b) => {
|
|
47
|
+
const pa = this.people.get(a), pb = this.people.get(b);
|
|
48
|
+
return (pa.birthOrder ?? 99) - (pb.birthOrder ?? 99) || (pa.birthDate ?? "").localeCompare(pb.birthDate ?? "");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
generationOf(id) {
|
|
52
|
+
return this.gen.get(id) ?? 0;
|
|
53
|
+
}
|
|
54
|
+
// Assign each person a generation index, then align partners to the deeper
|
|
55
|
+
// generation and push descendants below their parents until stable.
|
|
56
|
+
computeGenerations() {
|
|
57
|
+
const visit = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
58
|
+
if (this.gen.has(id)) return this.gen.get(id);
|
|
59
|
+
if (seen.has(id)) return 0;
|
|
60
|
+
seen.add(id);
|
|
61
|
+
const ps = this.parentsOf(id);
|
|
62
|
+
const val = ps.length ? Math.max(...ps.map((p) => visit(p, seen))) + 1 : 0;
|
|
63
|
+
this.gen.set(id, val);
|
|
64
|
+
return val;
|
|
65
|
+
};
|
|
66
|
+
for (const p of this.doc.people) visit(p.id);
|
|
67
|
+
for (let pass = 0; pass < this.doc.people.length; pass++) {
|
|
68
|
+
let changed = false;
|
|
69
|
+
for (const u of this.unions) {
|
|
70
|
+
const [a, b] = u.partnerIds;
|
|
71
|
+
const m = Math.max(this.gen.get(a), this.gen.get(b));
|
|
72
|
+
if (this.gen.get(a) !== m || this.gen.get(b) !== m) {
|
|
73
|
+
this.gen.set(a, m);
|
|
74
|
+
this.gen.set(b, m);
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const pc of this.parentChild) {
|
|
79
|
+
const parents = this.parentsOf(pc.childId);
|
|
80
|
+
const pg = Math.max(0, ...parents.map((p) => this.gen.get(p)));
|
|
81
|
+
if (parents.length && this.gen.get(pc.childId) <= pg) {
|
|
82
|
+
this.gen.set(pc.childId, pg + 1);
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!changed) break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/constants.ts
|
|
92
|
+
var NODE = 46;
|
|
93
|
+
var R = NODE / 2;
|
|
94
|
+
var GEN_SPACING = 150;
|
|
95
|
+
var SIB_STEP = 128;
|
|
96
|
+
var SPOUSE_HALF = 64;
|
|
97
|
+
var MARGIN = 70;
|
|
98
|
+
var LABEL_WRAP = 16;
|
|
99
|
+
var LABEL_LINE_H = 13;
|
|
100
|
+
|
|
101
|
+
// src/layout.ts
|
|
102
|
+
var Positions = class {
|
|
103
|
+
constructor(graph, xOf) {
|
|
104
|
+
this.graph = graph;
|
|
105
|
+
this.xOf = xOf;
|
|
106
|
+
}
|
|
107
|
+
graph;
|
|
108
|
+
xOf;
|
|
109
|
+
cx(id) {
|
|
110
|
+
return this.xOf.get(id) + MARGIN + R;
|
|
111
|
+
}
|
|
112
|
+
cy(id) {
|
|
113
|
+
return this.graph.generationOf(id) * GEN_SPACING + MARGIN + R;
|
|
114
|
+
}
|
|
115
|
+
bounds() {
|
|
116
|
+
const ids = this.graph.roster.map((p) => p.id);
|
|
117
|
+
return {
|
|
118
|
+
width: Math.max(...ids.map((id) => this.cx(id))) + MARGIN + 60,
|
|
119
|
+
height: Math.max(...ids.map((id) => this.cy(id))) + MARGIN + 60
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var GenerationalLayout = class {
|
|
124
|
+
graph;
|
|
125
|
+
xOf;
|
|
126
|
+
placedUnion;
|
|
127
|
+
cursor = 0;
|
|
128
|
+
layout(graph) {
|
|
129
|
+
this.graph = graph;
|
|
130
|
+
this.xOf = /* @__PURE__ */ new Map();
|
|
131
|
+
this.placedUnion = /* @__PURE__ */ new Set();
|
|
132
|
+
this.cursor = 0;
|
|
133
|
+
const roots = graph.roster.map((p) => p.id).filter((id) => graph.parentsOf(id).length === 0).sort((a, b) => (graph.unionHeadedBy(a) ? 0 : 1) - (graph.unionHeadedBy(b) ? 0 : 1));
|
|
134
|
+
for (const id of roots) this.placeSubtree(id);
|
|
135
|
+
for (const p of graph.roster) if (!this.xOf.has(p.id)) this.placeSubtree(p.id);
|
|
136
|
+
for (const pos of graph.doc.layout?.positions ?? []) {
|
|
137
|
+
this.xOf.set(pos.personId, pos.x);
|
|
138
|
+
}
|
|
139
|
+
return new Positions(graph, this.xOf);
|
|
140
|
+
}
|
|
141
|
+
placeSubtree(id) {
|
|
142
|
+
const existing = this.xOf.get(id);
|
|
143
|
+
if (existing !== void 0) return existing;
|
|
144
|
+
const union = this.graph.unionHeadedBy(id);
|
|
145
|
+
if (union && !this.placedUnion.has(union.id)) return this.placeUnion(union, id);
|
|
146
|
+
const kids = this.orderSiblings(this.graph.singleChildrenOf(id));
|
|
147
|
+
if (kids.length) {
|
|
148
|
+
const x2 = this.centerOver(kids);
|
|
149
|
+
this.xOf.set(id, x2);
|
|
150
|
+
return x2;
|
|
151
|
+
}
|
|
152
|
+
const x = this.cursor;
|
|
153
|
+
this.cursor += SIB_STEP;
|
|
154
|
+
this.xOf.set(id, x);
|
|
155
|
+
return x;
|
|
156
|
+
}
|
|
157
|
+
placeUnion(union, anchor) {
|
|
158
|
+
this.placedUnion.add(union.id);
|
|
159
|
+
const other = union.partnerIds.find((p) => p !== anchor);
|
|
160
|
+
const kids = this.orderSiblings(this.graph.childrenOfUnion(union.id));
|
|
161
|
+
let center;
|
|
162
|
+
if (kids.length) {
|
|
163
|
+
center = this.centerOver(kids);
|
|
164
|
+
} else {
|
|
165
|
+
center = this.cursor + SIB_STEP / 2;
|
|
166
|
+
this.cursor += SIB_STEP * 2;
|
|
167
|
+
}
|
|
168
|
+
this.xOf.set(anchor, center - SPOUSE_HALF);
|
|
169
|
+
this.xOf.set(other, center + SPOUSE_HALF);
|
|
170
|
+
this.cursor = Math.max(this.cursor, center + SPOUSE_HALF + SIB_STEP);
|
|
171
|
+
return center;
|
|
172
|
+
}
|
|
173
|
+
centerOver(ids) {
|
|
174
|
+
const xs = ids.map((k) => this.placeSubtree(k));
|
|
175
|
+
return (xs[0] + xs[xs.length - 1]) / 2;
|
|
176
|
+
}
|
|
177
|
+
// Birth order, but children who themselves head a union (bringing in a
|
|
178
|
+
// married-in spouse and that spouse's own cluster) are placed last so the
|
|
179
|
+
// horizontal expansion happens at the group's edge instead of splitting the
|
|
180
|
+
// blood-sibling row.
|
|
181
|
+
orderSiblings(ids) {
|
|
182
|
+
const sorted = this.graph.sortByBirth(ids);
|
|
183
|
+
const singles = sorted.filter((id) => !this.graph.unionHeadedBy(id));
|
|
184
|
+
const married = sorted.filter((id) => this.graph.unionHeadedBy(id));
|
|
185
|
+
return [...singles, ...married];
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
export {
|
|
189
|
+
GEN_SPACING,
|
|
190
|
+
GenerationalLayout,
|
|
191
|
+
GenogramGraph,
|
|
192
|
+
LABEL_LINE_H,
|
|
193
|
+
LABEL_WRAP,
|
|
194
|
+
MARGIN,
|
|
195
|
+
NODE,
|
|
196
|
+
Positions,
|
|
197
|
+
R,
|
|
198
|
+
SIB_STEP,
|
|
199
|
+
SPOUSE_HALF
|
|
200
|
+
};
|
|
201
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph.ts","../src/constants.ts","../src/layout.ts"],"sourcesContent":["// GenogramGraph: an indexed, query-friendly view over a raw genogram document.\n// Owns the family-structure domain logic (who are the parents, children, unions)\n// and the generation (vertical rank) computation. Rendering lives elsewhere.\n\nimport type {\n Genogram,\n Person,\n Relationship,\n UnionRelationship,\n ParentChildRelationship,\n EmotionalRelationship,\n} from \"@famtree/schema\"\n\nconst isUnion = (r: Relationship): r is UnionRelationship => r.type === \"union\"\nconst isParentChild = (r: Relationship): r is ParentChildRelationship => r.type === \"parent-child\"\nconst isEmotional = (r: Relationship): r is EmotionalRelationship => r.type === \"emotional\"\n\nclass GenogramGraph {\n readonly people: Map<string, Person>\n readonly unions: UnionRelationship[]\n readonly parentChild: ParentChildRelationship[]\n readonly emotions: EmotionalRelationship[]\n\n private readonly unionById: Map<string, UnionRelationship>\n private readonly pcByChild: Map<string, ParentChildRelationship>\n private readonly gen = new Map<string, number>()\n\n constructor(readonly doc: Genogram) {\n this.people = new Map(doc.people.map((p) => [p.id, p]))\n this.unions = doc.relationships.filter(isUnion)\n this.parentChild = doc.relationships.filter(isParentChild)\n this.emotions = doc.relationships.filter(isEmotional)\n this.unionById = new Map(this.unions.map((u) => [u.id, u]))\n this.pcByChild = new Map(this.parentChild.map((pc) => [pc.childId, pc]))\n this.computeGenerations()\n }\n\n get roster(): Person[] {\n return this.doc.people\n }\n\n parentLinkOf(id: string): ParentChildRelationship | undefined {\n return this.pcByChild.get(id)\n }\n\n parentsOf(id: string): string[] {\n const pc = this.pcByChild.get(id)\n if (!pc) return []\n if (pc.unionId) return this.unionById.get(pc.unionId)?.partnerIds ?? []\n return pc.parentIds ?? []\n }\n\n unionHeadedBy(id: string): UnionRelationship | undefined {\n return this.unions.find((u) => u.partnerIds.includes(id))\n }\n\n childrenOfUnion(uid: string): string[] {\n return this.parentChild.filter((pc) => pc.unionId === uid).map((pc) => pc.childId)\n }\n\n singleChildrenOf(id: string): string[] {\n return this.parentChild.filter((pc) => !pc.unionId && (pc.parentIds ?? []).includes(id)).map((pc) => pc.childId)\n }\n\n sortByBirth(ids: string[]): string[] {\n return [...ids].sort((a, b) => {\n const pa = this.people.get(a)!,\n pb = this.people.get(b)!\n return (pa.birthOrder ?? 99) - (pb.birthOrder ?? 99) || (pa.birthDate ?? \"\").localeCompare(pb.birthDate ?? \"\")\n })\n }\n\n generationOf(id: string): number {\n return this.gen.get(id) ?? 0\n }\n\n // Assign each person a generation index, then align partners to the deeper\n // generation and push descendants below their parents until stable.\n private computeGenerations(): void {\n const visit = (id: string, seen = new Set<string>()): number => {\n if (this.gen.has(id)) return this.gen.get(id)!\n if (seen.has(id)) return 0\n seen.add(id)\n const ps = this.parentsOf(id)\n const val = ps.length ? Math.max(...ps.map((p) => visit(p, seen))) + 1 : 0\n this.gen.set(id, val)\n return val\n }\n for (const p of this.doc.people) visit(p.id)\n\n for (let pass = 0; pass < this.doc.people.length; pass++) {\n let changed = false\n for (const u of this.unions) {\n const [a, b] = u.partnerIds\n const m = Math.max(this.gen.get(a)!, this.gen.get(b)!)\n if (this.gen.get(a) !== m || this.gen.get(b) !== m) {\n this.gen.set(a, m)\n this.gen.set(b, m)\n changed = true\n }\n }\n for (const pc of this.parentChild) {\n const parents = this.parentsOf(pc.childId)\n const pg = Math.max(0, ...parents.map((p) => this.gen.get(p)!))\n if (parents.length && this.gen.get(pc.childId)! <= pg) {\n this.gen.set(pc.childId, pg + 1)\n changed = true\n }\n }\n if (!changed) break\n }\n }\n}\n\nexport { GenogramGraph }\n","// Shared geometry constants for layout and rendering.\nconst NODE = 46 // node width / diameter\nconst R = NODE / 2 // node radius\nconst GEN_SPACING = 150 // vertical distance between generations\nconst SIB_STEP = 128 // horizontal step between sibling subtrees\nconst SPOUSE_HALF = 64 // half distance between partners in a couple\nconst MARGIN = 70 // outer margin / frame padding\nconst LABEL_WRAP = 16 // max characters per label line before wrapping\nconst LABEL_LINE_H = 13 // vertical distance between wrapped label lines\n\nexport { NODE, R, GEN_SPACING, SIB_STEP, SPOUSE_HALF, MARGIN, LABEL_WRAP, LABEL_LINE_H }\n","// Layout: turns a GenogramGraph into concrete x/y coordinates.\n// `LayoutEngine` is an abstraction so the renderer can depend on it rather than a\n// concrete algorithm (Dependency Inversion / Open-Closed for new strategies).\n\nimport type { GenogramGraph } from \"./graph\"\nimport type { UnionRelationship } from \"@famtree/schema\"\nimport { GEN_SPACING, MARGIN, R, SIB_STEP, SPOUSE_HALF } from \"./constants\"\n\n/** Resolved coordinates for a laid-out genogram. */\nclass Positions {\n constructor(\n private readonly graph: GenogramGraph,\n private readonly xOf: Map<string, number>,\n ) {}\n\n cx(id: string): number {\n return this.xOf.get(id)! + MARGIN + R\n }\n\n cy(id: string): number {\n return this.graph.generationOf(id) * GEN_SPACING + MARGIN + R\n }\n\n bounds(): { width: number; height: number } {\n const ids = this.graph.roster.map((p) => p.id)\n return {\n width: Math.max(...ids.map((id) => this.cx(id))) + MARGIN + 60,\n height: Math.max(...ids.map((id) => this.cy(id))) + MARGIN + 60,\n }\n }\n}\n\ninterface LayoutEngine {\n layout(graph: GenogramGraph): Positions\n}\n\n/**\n * Places couples centered above their children, oldest sibling to the left,\n * married-in spouses to the right of the blood relative. Honors explicit\n * `layout.positions` overrides from the document.\n */\nclass GenerationalLayout implements LayoutEngine {\n private graph!: GenogramGraph\n private xOf!: Map<string, number>\n private placedUnion!: Set<string>\n private cursor = 0\n\n layout(graph: GenogramGraph): Positions {\n this.graph = graph\n this.xOf = new Map()\n this.placedUnion = new Set()\n this.cursor = 0\n\n // Place ancestors (people with no parents) first; couples before singles.\n const roots = graph.roster\n .map((p) => p.id)\n .filter((id) => graph.parentsOf(id).length === 0)\n .sort((a, b) => (graph.unionHeadedBy(a) ? 0 : 1) - (graph.unionHeadedBy(b) ? 0 : 1))\n for (const id of roots) this.placeSubtree(id)\n for (const p of graph.roster) if (!this.xOf.has(p.id)) this.placeSubtree(p.id)\n\n for (const pos of graph.doc.layout?.positions ?? []) {\n this.xOf.set(pos.personId, pos.x)\n }\n return new Positions(graph, this.xOf)\n }\n\n private placeSubtree(id: string): number {\n const existing = this.xOf.get(id)\n if (existing !== undefined) return existing\n\n const union = this.graph.unionHeadedBy(id)\n if (union && !this.placedUnion.has(union.id)) return this.placeUnion(union, id)\n\n const kids = this.orderSiblings(this.graph.singleChildrenOf(id))\n if (kids.length) {\n const x = this.centerOver(kids)\n this.xOf.set(id, x)\n return x\n }\n const x = this.cursor\n this.cursor += SIB_STEP\n this.xOf.set(id, x)\n return x\n }\n\n private placeUnion(union: UnionRelationship, anchor: string): number {\n this.placedUnion.add(union.id)\n const other = union.partnerIds.find((p) => p !== anchor)!\n const kids = this.orderSiblings(this.graph.childrenOfUnion(union.id))\n\n let center: number\n if (kids.length) {\n center = this.centerOver(kids)\n } else {\n center = this.cursor + SIB_STEP / 2\n this.cursor += SIB_STEP * 2\n }\n this.xOf.set(anchor, center - SPOUSE_HALF)\n this.xOf.set(other, center + SPOUSE_HALF)\n this.cursor = Math.max(this.cursor, center + SPOUSE_HALF + SIB_STEP)\n return center\n }\n\n private centerOver(ids: string[]): number {\n const xs = ids.map((k) => this.placeSubtree(k))\n return (xs[0] + xs[xs.length - 1]) / 2\n }\n\n // Birth order, but children who themselves head a union (bringing in a\n // married-in spouse and that spouse's own cluster) are placed last so the\n // horizontal expansion happens at the group's edge instead of splitting the\n // blood-sibling row.\n private orderSiblings(ids: string[]): string[] {\n const sorted = this.graph.sortByBirth(ids)\n const singles = sorted.filter((id) => !this.graph.unionHeadedBy(id))\n const married = sorted.filter((id) => this.graph.unionHeadedBy(id))\n return [...singles, ...married]\n }\n}\n\nexport { Positions, GenerationalLayout }\nexport type { LayoutEngine }\n"],"mappings":"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","names":["x"]}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GenogramGraph } from "./graph";
|
|
2
|
+
/** Resolved coordinates for a laid-out genogram. */
|
|
3
|
+
declare class Positions {
|
|
4
|
+
private readonly graph;
|
|
5
|
+
private readonly xOf;
|
|
6
|
+
constructor(graph: GenogramGraph, xOf: Map<string, number>);
|
|
7
|
+
cx(id: string): number;
|
|
8
|
+
cy(id: string): number;
|
|
9
|
+
bounds(): {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface LayoutEngine {
|
|
15
|
+
layout(graph: GenogramGraph): Positions;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Places couples centered above their children, oldest sibling to the left,
|
|
19
|
+
* married-in spouses to the right of the blood relative. Honors explicit
|
|
20
|
+
* `layout.positions` overrides from the document.
|
|
21
|
+
*/
|
|
22
|
+
declare class GenerationalLayout implements LayoutEngine {
|
|
23
|
+
private graph;
|
|
24
|
+
private xOf;
|
|
25
|
+
private placedUnion;
|
|
26
|
+
private cursor;
|
|
27
|
+
layout(graph: GenogramGraph): Positions;
|
|
28
|
+
private placeSubtree;
|
|
29
|
+
private placeUnion;
|
|
30
|
+
private centerOver;
|
|
31
|
+
private orderSiblings;
|
|
32
|
+
}
|
|
33
|
+
export { Positions, GenerationalLayout };
|
|
34
|
+
export type { LayoutEngine };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@famtree/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Runtime-agnostic genogram engine: domain graph and layout.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Felipe Plets",
|
|
8
|
+
"homepage": "https://github.com/felipeplets/famtree#readme",
|
|
9
|
+
"bugs": "https://github.com/felipeplets/famtree/issues",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/felipeplets/famtree.git",
|
|
13
|
+
"directory": "libs/core"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": { "access": "public" },
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": ["dist"],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@famtree/schema": "workspace:0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
32
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
33
|
+
}
|
|
34
|
+
}
|