compasso 0.2.0 → 0.4.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.
Files changed (77) hide show
  1. package/README.md +126 -7
  2. package/dist/chunk-F47C6ZEB.js +1041 -0
  3. package/dist/chunk-F47C6ZEB.js.map +1 -0
  4. package/dist/chunk-JP4N42AY.js +497 -0
  5. package/dist/chunk-JP4N42AY.js.map +1 -0
  6. package/dist/{chunk-P2S7AUOL.js → chunk-LRHHUJFZ.js} +3 -3
  7. package/dist/{chunk-P2S7AUOL.js.map → chunk-LRHHUJFZ.js.map} +1 -1
  8. package/dist/{chunk-5B453C4P.js → chunk-O3BT2O42.js} +32 -3
  9. package/dist/chunk-O3BT2O42.js.map +1 -0
  10. package/dist/{chunk-EHQMKVDM.js → chunk-Q6DVTCXD.js} +9 -24
  11. package/dist/chunk-Q6DVTCXD.js.map +1 -0
  12. package/dist/{chunk-5PGOL2KR.js → chunk-RWPGGWO5.js} +9 -28
  13. package/dist/chunk-RWPGGWO5.js.map +1 -0
  14. package/dist/chunk-UJVU7B44.js +764 -0
  15. package/dist/chunk-UJVU7B44.js.map +1 -0
  16. package/dist/{chunk-TP3JOOJW.js → chunk-ZBDABVIO.js} +3 -3
  17. package/dist/{chunk-TP3JOOJW.js.map → chunk-ZBDABVIO.js.map} +1 -1
  18. package/dist/core/index.cjs +30 -0
  19. package/dist/core/index.cjs.map +1 -1
  20. package/dist/core/index.d.cts +5 -1
  21. package/dist/core/index.d.ts +5 -1
  22. package/dist/core/index.js +1 -1
  23. package/dist/ecomap/index.cjs +32 -21
  24. package/dist/ecomap/index.cjs.map +1 -1
  25. package/dist/ecomap/index.js +2 -2
  26. package/dist/fault-tree/index.js +2 -2
  27. package/dist/fishbone/index.js +2 -2
  28. package/dist/genogram/index.cjs +36 -25
  29. package/dist/genogram/index.cjs.map +1 -1
  30. package/dist/genogram/index.d.cts +4 -2
  31. package/dist/genogram/index.d.ts +4 -2
  32. package/dist/genogram/index.js +2 -2
  33. package/dist/index.cjs +2397 -55
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +9 -2
  36. package/dist/index.d.ts +9 -2
  37. package/dist/index.js +8 -5
  38. package/dist/kinship-DqEklrDN.d.ts +51 -0
  39. package/dist/kinship-Dy_ijjJV.d.cts +51 -0
  40. package/dist/labels-CBQ_3Ec9.d.cts +123 -0
  41. package/dist/labels-DNqRkWuI.d.ts +123 -0
  42. package/dist/labels-RtFw9tX1.d.cts +91 -0
  43. package/dist/labels-RtFw9tX1.d.ts +91 -0
  44. package/dist/labels-iZjijjtK.d.cts +64 -0
  45. package/dist/labels-iZjijjtK.d.ts +64 -0
  46. package/dist/locales/pt-br.cjs +77 -0
  47. package/dist/locales/pt-br.cjs.map +1 -1
  48. package/dist/locales/pt-br.d.cts +12 -2
  49. package/dist/locales/pt-br.d.ts +12 -2
  50. package/dist/locales/pt-br.js +72 -1
  51. package/dist/locales/pt-br.js.map +1 -1
  52. package/dist/org-chart/index.cjs +853 -0
  53. package/dist/org-chart/index.cjs.map +1 -0
  54. package/dist/org-chart/index.d.cts +168 -0
  55. package/dist/org-chart/index.d.ts +168 -0
  56. package/dist/org-chart/index.js +4 -0
  57. package/dist/org-chart/index.js.map +1 -0
  58. package/dist/pedigree/index.cjs +1151 -0
  59. package/dist/pedigree/index.cjs.map +1 -0
  60. package/dist/pedigree/index.d.cts +155 -0
  61. package/dist/pedigree/index.d.ts +155 -0
  62. package/dist/pedigree/index.js +4 -0
  63. package/dist/pedigree/index.js.map +1 -0
  64. package/dist/phylo/index.cjs +553 -0
  65. package/dist/phylo/index.cjs.map +1 -0
  66. package/dist/phylo/index.d.cts +158 -0
  67. package/dist/phylo/index.d.ts +158 -0
  68. package/dist/phylo/index.js +4 -0
  69. package/dist/phylo/index.js.map +1 -0
  70. package/dist/types-BnMG7TCd.d.cts +66 -0
  71. package/dist/types-BnMG7TCd.d.ts +66 -0
  72. package/package.json +42 -3
  73. package/dist/chunk-5B453C4P.js.map +0 -1
  74. package/dist/chunk-5PGOL2KR.js.map +0 -1
  75. package/dist/chunk-EHQMKVDM.js.map +0 -1
  76. package/dist/kinship-BARO5-qz.d.cts +0 -115
  77. package/dist/kinship-Bkf87Jhu.d.ts +0 -115
@@ -0,0 +1,1041 @@
1
+ import { clampLabel, romanNumeral, estimateTextWidth, FONT_FAMILY, xmlEscape, legendBlock, wrapLabelBalanced, LEGEND_SWATCH_W } from './chunk-O3BT2O42.js';
2
+
3
+ // src/pedigree/types.ts
4
+ var LIFE_STATUSES = ["alive", "stillbirth"];
5
+
6
+ // src/pedigree/labels.ts
7
+ var PEDIGREE_TITLE_LABELS_EN = {
8
+ affected: "affected",
9
+ carrier: "carrier",
10
+ deceased: "deceased",
11
+ proband: "proband",
12
+ consultand: "consultand",
13
+ consanguineous: "consanguineous mating",
14
+ mating: "mating",
15
+ sibship: "sibship",
16
+ twins: {
17
+ mz: "monozygotic twins",
18
+ dz: "dizygotic twins",
19
+ unknown: "twins, zygosity unknown"
20
+ },
21
+ stillbirth: "stillbirth",
22
+ generation: "generation"
23
+ };
24
+ var PEDIGREE_SVG_LABELS_EN = {
25
+ shapes: {
26
+ square: "Male",
27
+ circle: "Female",
28
+ diamond: "Sex unknown"
29
+ },
30
+ unaffected: "Unaffected",
31
+ carrier: "Carrier",
32
+ deceased: "Deceased",
33
+ proband: "Proband",
34
+ consultand: "Consultand",
35
+ consanguineous: "Consanguineous mating",
36
+ twins: {
37
+ mz: "Monozygotic twins",
38
+ dz: "Dizygotic twins",
39
+ unknown: "Twins (zygosity unknown)"
40
+ },
41
+ stillbirth: "Stillbirth",
42
+ isolated: "No recorded relatives",
43
+ ariaLabel: "Pedigree chart"
44
+ };
45
+
46
+ // src/pedigree/validate.ts
47
+ var PedigreeValidationError = class extends Error {
48
+ issues;
49
+ constructor(issues) {
50
+ super(`invalid pedigree: ${issues.map((i) => i.message).join("; ")}`);
51
+ this.name = "PedigreeValidationError";
52
+ this.issues = issues;
53
+ }
54
+ };
55
+ var MAX_CONDITIONS_PER_INDIVIDUAL = 4;
56
+ function sortIssues(issues) {
57
+ const unique = /* @__PURE__ */ new Map();
58
+ for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
59
+ return [...unique.values()].sort(
60
+ (a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
61
+ );
62
+ }
63
+ function pedigreeIssues(input) {
64
+ if (input.individuals.length === 0 && input.matings.length === 0 && input.sibships.length === 0) return [];
65
+ const issues = [];
66
+ const push = (code, message) => {
67
+ issues.push({ code, message });
68
+ };
69
+ const individualById = /* @__PURE__ */ new Map();
70
+ const dupIndividualIds = /* @__PURE__ */ new Set();
71
+ for (const ind of input.individuals) {
72
+ if (individualById.has(ind.id)) dupIndividualIds.add(ind.id);
73
+ else individualById.set(ind.id, ind);
74
+ }
75
+ for (const id of [...dupIndividualIds].sort((a, b) => a - b)) {
76
+ push("duplicate-id", `duplicate individual id ${id}`);
77
+ }
78
+ const matingById = /* @__PURE__ */ new Map();
79
+ const dupMatingIds = /* @__PURE__ */ new Set();
80
+ for (const m of input.matings) {
81
+ if (matingById.has(m.id)) dupMatingIds.add(m.id);
82
+ else matingById.set(m.id, m);
83
+ }
84
+ for (const id of [...dupMatingIds].sort((a, b) => a - b)) {
85
+ push("duplicate-id", `duplicate mating id ${id}`);
86
+ }
87
+ const sibshipById = /* @__PURE__ */ new Map();
88
+ const dupSibshipIds = /* @__PURE__ */ new Set();
89
+ for (const s of input.sibships) {
90
+ if (sibshipById.has(s.id)) dupSibshipIds.add(s.id);
91
+ else sibshipById.set(s.id, s);
92
+ }
93
+ for (const id of [...dupSibshipIds].sort((a, b) => a - b)) {
94
+ push("duplicate-id", `duplicate sibship id ${id}`);
95
+ }
96
+ if (issues.length > 0) {
97
+ return sortIssues(issues);
98
+ }
99
+ const individuals = [...individualById.values()].sort((a, b) => a.id - b.id);
100
+ const matings = [...matingById.values()].sort((a, b) => a.id - b.id);
101
+ const sibships = [...sibshipById.values()].sort((a, b) => a.id - b.id);
102
+ const conditionIds = new Set(input.conditions.map((c) => c.id));
103
+ for (const m of matings) {
104
+ if (!individualById.has(m.partnerAId)) {
105
+ push("unknown-partner", `mating ${m.id} references unknown individual ${m.partnerAId}`);
106
+ }
107
+ if (!individualById.has(m.partnerBId)) {
108
+ push("unknown-partner", `mating ${m.id} references unknown individual ${m.partnerBId}`);
109
+ }
110
+ if (m.partnerAId === m.partnerBId) {
111
+ push("self-mating", `mating ${m.id} mates individual ${m.partnerAId} with itself`);
112
+ }
113
+ }
114
+ for (const s of sibships) {
115
+ if (!matingById.has(s.matingId)) {
116
+ push("unknown-sibship-mating", `sibship ${s.id} references unknown mating ${s.matingId}`);
117
+ }
118
+ for (const childId of s.childIds) {
119
+ if (!individualById.has(childId)) {
120
+ push("unknown-child", `sibship ${s.id} references unknown child ${childId}`);
121
+ }
122
+ }
123
+ const childSet = new Set(s.childIds);
124
+ const offending = /* @__PURE__ */ new Set();
125
+ for (const tg of s.twinGroups) {
126
+ for (const memberId of tg.childIds) {
127
+ if (!childSet.has(memberId)) offending.add(memberId);
128
+ }
129
+ }
130
+ for (const memberId of [...offending].sort((a, b) => a - b)) {
131
+ push("twin-not-in-sibship", `twin ${memberId} is not a child of sibship ${s.id}`);
132
+ }
133
+ for (const tg of s.twinGroups) {
134
+ if (tg.childIds.length < 2) {
135
+ const [only] = tg.childIds;
136
+ push(
137
+ "twin-group-too-small",
138
+ `twin group in sibship ${s.id} has only 1 member (${only ?? "none"}) \u2014 at least 2 required`
139
+ );
140
+ }
141
+ }
142
+ for (const tg of s.twinGroups) {
143
+ if (tg.childIds.length < 2) continue;
144
+ const memberSet = new Set(tg.childIds);
145
+ const positions = s.childIds.map((cid, pos) => memberSet.has(cid) ? pos : -1).filter((p) => p >= 0);
146
+ if (positions.length === 0) continue;
147
+ const first = positions[0];
148
+ const last = positions[positions.length - 1];
149
+ if (last - first !== positions.length - 1) {
150
+ const memberList = [...tg.childIds].sort((a, b) => a - b).join(", ");
151
+ push(
152
+ "twin-group-not-contiguous",
153
+ `twin group [${memberList}] in sibship ${s.id} is not a contiguous run in childIds`
154
+ );
155
+ }
156
+ }
157
+ }
158
+ const sibshipsOfChild = /* @__PURE__ */ new Map();
159
+ for (const s of sibships) {
160
+ for (const childId of s.childIds) {
161
+ const arr = sibshipsOfChild.get(childId) ?? [];
162
+ arr.push(s.id);
163
+ sibshipsOfChild.set(childId, arr);
164
+ }
165
+ }
166
+ for (const [childId, sibIds] of [...sibshipsOfChild.entries()].sort((a, b) => a[0] - b[0])) {
167
+ if (sibIds.length > 1) {
168
+ push(
169
+ "child-of-two-sibships",
170
+ `individual ${childId} is a child of sibships ${[...sibIds].sort((a, b) => a - b).join(", ")}`
171
+ );
172
+ }
173
+ }
174
+ for (const ind of individuals) {
175
+ if (!Number.isInteger(ind.generation)) {
176
+ push("generation-not-integer", `individual ${ind.id} has non-integer generation ${ind.generation}`);
177
+ }
178
+ }
179
+ for (const ind of individuals) {
180
+ for (const conditionId of ind.affectedBy) {
181
+ if (!conditionIds.has(conditionId)) {
182
+ push("unknown-condition", `individual ${ind.id} is affected by unknown condition ${conditionId}`);
183
+ }
184
+ }
185
+ if (ind.affectedBy.length > MAX_CONDITIONS_PER_INDIVIDUAL) {
186
+ push(
187
+ "too-many-conditions",
188
+ `individual ${ind.id} has ${ind.affectedBy.length} conditions \u2014 at most ${MAX_CONDITIONS_PER_INDIVIDUAL} can be drawn`
189
+ );
190
+ }
191
+ }
192
+ const GRAPH_BLOCKING = /* @__PURE__ */ new Set([
193
+ "duplicate-id",
194
+ "unknown-partner",
195
+ "unknown-sibship-mating",
196
+ "unknown-child"
197
+ ]);
198
+ if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
199
+ for (const s of sibships) {
200
+ const mating = matingById.get(s.matingId);
201
+ if (mating === void 0) continue;
202
+ const a = individualById.get(mating.partnerAId);
203
+ const b = individualById.get(mating.partnerBId);
204
+ const parentGenerations = [a, b].filter((p) => p !== void 0 && Number.isInteger(p.generation)).map((p) => p.generation);
205
+ if (parentGenerations.length === 0) continue;
206
+ const parentRow = Math.max(...parentGenerations);
207
+ for (const childId of s.childIds) {
208
+ const child = individualById.get(childId);
209
+ if (child === void 0 || !Number.isInteger(child.generation)) continue;
210
+ if (child.generation <= parentRow) {
211
+ push(
212
+ "generation-inversion",
213
+ `child ${childId} (generation ${child.generation}) is not below its parents (generation ${parentRow}) in sibship ${s.id}`
214
+ );
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return sortIssues(issues);
220
+ }
221
+ function validatePedigree(input) {
222
+ const issues = pedigreeIssues(input);
223
+ if (issues.length > 0) throw new PedigreeValidationError(issues);
224
+ }
225
+
226
+ // src/pedigree/layout.ts
227
+ var PED_GLYPH = 40;
228
+ var PED_LABEL_FONT = 12;
229
+ var PED_LABEL_LINE_H = 14;
230
+ var PED_ADDRESS_FONT = 10;
231
+ var PED_LABEL_GAP = 6;
232
+ var PADDING = 32;
233
+ var GUTTER = 36;
234
+ var H_GAP = 28;
235
+ var MATING_RUN = 22;
236
+ var CORRIDOR = 44;
237
+ var TWIN_JUNCTION_DROP = 12;
238
+ var BRIDGE_DIP_GAP = 10;
239
+ var BRIDGE_DIP_STEP = 7;
240
+ var BRIDGE_STUB_X_STEP = 3;
241
+ var PED_MATING_ID_BASE = 1e6;
242
+ var PED_DESCENT_ID_BASE = 2e6;
243
+ var PED_SIBBAR_ID_BASE = 3e6;
244
+ var PED_RISER_ID_BASE = 4e6;
245
+ var PED_TWINBAR_ID_BASE = 5e6;
246
+ var round = (n) => Math.round(n * 100) / 100;
247
+ function shapeForSex(sex) {
248
+ if (sex === "male") return "square";
249
+ if (sex === "female") return "circle";
250
+ return "diamond";
251
+ }
252
+ var PED_CONDITION_FILLS = ["#52525b", "#a1a1aa", "#3f3f46", "#71717a"];
253
+ function wrapPedigreeLabel(displayLabel) {
254
+ if (displayLabel === "") return [];
255
+ return wrapLabelBalanced(displayLabel, 2);
256
+ }
257
+ function individualTitle(ind, address, conditionLabelById, titleLabels) {
258
+ if (ind.title !== void 0) return ind.title;
259
+ const parts = [`${address} \xB7 ${ind.label}`];
260
+ if (ind.affectedBy.length > 0) {
261
+ const names = ind.affectedBy.map((id) => conditionLabelById.get(id) ?? String(id));
262
+ parts.push(`${titleLabels.affected}: ${names.join(", ")}`);
263
+ }
264
+ if (ind.carrier && ind.affectedBy.length === 0) parts.push(titleLabels.carrier);
265
+ if (ind.deceased) parts.push(titleLabels.deceased);
266
+ if (ind.role === "proband") parts.push(titleLabels.proband);
267
+ else if (ind.role === "consultand") parts.push(titleLabels.consultand);
268
+ if (ind.lifeStatus === "stillbirth") parts.push(titleLabels.stillbirth);
269
+ return parts.join(" \xB7 ");
270
+ }
271
+ function computePedigreeLayout(input, opts = {}) {
272
+ if (input.individuals.length === 0 && input.matings.length === 0 && input.sibships.length === 0) {
273
+ return {
274
+ width: PADDING * 2,
275
+ height: PADDING * 2,
276
+ nodes: [],
277
+ elements: [],
278
+ generations: [],
279
+ conditionFills: [],
280
+ twinZygositiesUsed: [],
281
+ unknownTwinJunctions: [],
282
+ isolatedIndividualIds: []
283
+ };
284
+ }
285
+ validatePedigree(input);
286
+ const titleLabels = opts.titleLabels ?? PEDIGREE_TITLE_LABELS_EN;
287
+ const individuals = [...input.individuals].sort((a, b) => a.id - b.id);
288
+ const individualById = new Map(individuals.map((i) => [i.id, i]));
289
+ const matings = [...input.matings].sort((a, b) => a.id - b.id);
290
+ const sibships = [...input.sibships].sort((a, b) => a.id - b.id);
291
+ const conditionLabelById = new Map(input.conditions.map((c) => [c.id, c.label]));
292
+ const partnersOf = (m) => m.partnerAId <= m.partnerBId ? [m.partnerAId, m.partnerBId] : [m.partnerBId, m.partnerAId];
293
+ const distinctGenerations = [...new Set(individuals.map((i) => i.generation))].sort((a, b) => a - b);
294
+ const rowOfGeneration = /* @__PURE__ */ new Map();
295
+ distinctGenerations.forEach((g, i) => rowOfGeneration.set(g, i));
296
+ const rowCount = distinctGenerations.length;
297
+ const labelLinesOf = /* @__PURE__ */ new Map();
298
+ for (const ind of individuals) {
299
+ labelLinesOf.set(ind.id, wrapPedigreeLabel(clampLabel(ind.label, opts.maxLabelChars)));
300
+ }
301
+ const nodeHalfWidth = (ind) => {
302
+ const lines = labelLinesOf.get(ind.id) ?? [];
303
+ const labelW = lines.reduce((m, l) => Math.max(m, estimateTextWidth(l, PED_LABEL_FONT)), 0);
304
+ return Math.max(PED_GLYPH, labelW) / 2;
305
+ };
306
+ const childToSibship = /* @__PURE__ */ new Map();
307
+ for (const s of sibships) for (const c of s.childIds) childToSibship.set(c, s);
308
+ const sibshipByMating = /* @__PURE__ */ new Map();
309
+ for (const s of sibships) {
310
+ const arr = sibshipByMating.get(s.matingId) ?? [];
311
+ arr.push(s);
312
+ sibshipByMating.set(s.matingId, arr);
313
+ }
314
+ const matingsOfIndividual = /* @__PURE__ */ new Map();
315
+ for (const m of matings) {
316
+ for (const p of partnersOf(m)) {
317
+ const arr = matingsOfIndividual.get(p) ?? [];
318
+ arr.push(m);
319
+ matingsOfIndividual.set(p, arr);
320
+ }
321
+ }
322
+ const placedById = /* @__PURE__ */ new Map();
323
+ let cursorX = PADDING + GUTTER;
324
+ const placeLeaf = (ind, leftEdge) => {
325
+ const half = nodeHalfWidth(ind);
326
+ const cx = leftEdge + half;
327
+ const placed = { ind, cx, spanL: half, spanR: half };
328
+ placedById.set(ind.id, placed);
329
+ return placed;
330
+ };
331
+ const placedMatings = /* @__PURE__ */ new Set();
332
+ const matingCount = (id) => (matingsOfIndividual.get(id) ?? []).length;
333
+ const hubCursor = /* @__PURE__ */ new Map();
334
+ const hubSpouseSeq = /* @__PURE__ */ new Map();
335
+ const packMating = (mating, leftEdge) => {
336
+ placedMatings.add(mating.id);
337
+ const [aId, bId] = partnersOf(mating);
338
+ const a = individualById.get(aId);
339
+ const b = individualById.get(bId);
340
+ const aHalf = nodeHalfWidth(a);
341
+ const bHalf = nodeHalfWidth(b);
342
+ const halfSep = Math.max(aHalf, bHalf) + MATING_RUN;
343
+ const aPlaced = placedById.get(aId);
344
+ const bPlaced = placedById.get(bId);
345
+ const hubId = aPlaced !== void 0 ? aId : bPlaced !== void 0 ? bId : matingCount(aId) >= 2 ? aId : matingCount(bId) >= 2 ? bId : null;
346
+ const blockLeft = hubId !== null && hubCursor.has(hubId) ? hubCursor.get(hubId) : leftEdge;
347
+ const sibs = (sibshipByMating.get(mating.id) ?? []).sort((s1, s2) => s1.id - s2.id);
348
+ const childIds = sibs.flatMap((s) => s.childIds);
349
+ let childCursor = blockLeft;
350
+ let childLeft = Number.POSITIVE_INFINITY;
351
+ let childrenRight = blockLeft;
352
+ let placedAnyChild = false;
353
+ for (const childId of childIds) {
354
+ const child = individualById.get(childId);
355
+ if (child === void 0 || placedById.has(childId)) continue;
356
+ const downMating = (matingsOfIndividual.get(childId) ?? []).find((m) => {
357
+ if (placedMatings.has(m.id)) return false;
358
+ const [pa, pb] = partnersOf(m);
359
+ const otherId = pa === childId ? pb : pa;
360
+ const other = individualById.get(otherId);
361
+ return other === void 0 || child.generation <= other.generation;
362
+ });
363
+ if (downMating !== void 0) {
364
+ const right = packMating(downMating, childCursor);
365
+ childLeft = Math.min(childLeft, placedById.get(childId).cx - nodeHalfWidth(child));
366
+ childrenRight = right;
367
+ childCursor = right + H_GAP;
368
+ } else {
369
+ const placed = placeLeaf(child, childCursor);
370
+ childLeft = Math.min(childLeft, placed.cx - placed.spanL);
371
+ childrenRight = placed.cx + placed.spanR;
372
+ childCursor = childrenRight + H_GAP;
373
+ }
374
+ placedAnyChild = true;
375
+ }
376
+ const bandMid = placedAnyChild ? (childLeft + childrenRight) / 2 : blockLeft + halfSep;
377
+ if (hubId === null) {
378
+ const pairMid = placedAnyChild ? bandMid : blockLeft + Math.max(aHalf, bHalf) + halfSep;
379
+ placedById.set(aId, { ind: a, cx: pairMid - halfSep, spanL: aHalf, spanR: aHalf });
380
+ placedById.set(bId, { ind: b, cx: pairMid + halfSep, spanL: bHalf, spanR: bHalf });
381
+ } else {
382
+ const spouseId = hubId === aId ? bId : aId;
383
+ const spouse = individualById.get(spouseId);
384
+ const spouseHalf = nodeHalfWidth(spouse);
385
+ const hubHalf = nodeHalfWidth(individualById.get(hubId));
386
+ const seq = hubSpouseSeq.get(hubId) ?? 0;
387
+ const hubExisting = placedById.get(hubId);
388
+ const minSpouseCx = blockLeft + hubHalf + halfSep;
389
+ let spouseCx = placedAnyChild ? Math.max(bandMid, minSpouseCx) : minSpouseCx + spouseHalf;
390
+ if (hubExisting === void 0) {
391
+ placedById.set(hubId, { ind: individualById.get(hubId), cx: spouseCx - halfSep, spanL: hubHalf, spanR: hubHalf });
392
+ } else {
393
+ spouseCx = Math.max(spouseCx, hubExisting.cx + halfSep);
394
+ }
395
+ placedById.set(spouseId, { ind: spouse, cx: spouseCx, spanL: spouseHalf, spanR: spouseHalf });
396
+ hubSpouseSeq.set(hubId, seq + 1);
397
+ }
398
+ const blockRight = Math.max(
399
+ childrenRight,
400
+ placedById.get(aId).cx + nodeHalfWidth(a),
401
+ placedById.get(bId).cx + nodeHalfWidth(b)
402
+ );
403
+ if (hubId !== null) hubCursor.set(hubId, blockRight + H_GAP);
404
+ return blockRight;
405
+ };
406
+ const generationOfMating = (m) => {
407
+ const [aId, bId] = partnersOf(m);
408
+ const a = individualById.get(aId);
409
+ const b = individualById.get(bId);
410
+ const gens = [a, b].filter((p) => p !== void 0).map((p) => p.generation);
411
+ return gens.length > 0 ? Math.min(...gens) : 0;
412
+ };
413
+ const rootMatings = matings.filter((m) => {
414
+ const [aId, bId] = partnersOf(m);
415
+ return !childToSibship.has(aId) && !childToSibship.has(bId);
416
+ }).sort((m1, m2) => generationOfMating(m1) - generationOfMating(m2) || m1.id - m2.id);
417
+ for (const m of rootMatings) {
418
+ if (placedMatings.has(m.id)) continue;
419
+ const right = packMating(m, cursorX);
420
+ cursorX = right + H_GAP * 2;
421
+ }
422
+ for (const m of matings) {
423
+ if (placedMatings.has(m.id)) continue;
424
+ const right = packMating(m, cursorX);
425
+ cursorX = right + H_GAP * 2;
426
+ }
427
+ const inAnyMating = /* @__PURE__ */ new Set();
428
+ for (const m of matings) for (const p of partnersOf(m)) inAnyMating.add(p);
429
+ const isolatedIndividualIds = individuals.filter((i) => !inAnyMating.has(i.id) && !childToSibship.has(i.id)).map((i) => i.id);
430
+ for (const id of isolatedIndividualIds) {
431
+ const ind = individualById.get(id);
432
+ if (placedById.has(id)) continue;
433
+ placeLeaf(ind, cursorX);
434
+ cursorX = placedById.get(id).cx + nodeHalfWidth(ind) + H_GAP;
435
+ }
436
+ for (const ind of individuals) {
437
+ if (placedById.has(ind.id)) continue;
438
+ placeLeaf(ind, cursorX);
439
+ cursorX = placedById.get(ind.id).cx + nodeHalfWidth(ind) + H_GAP;
440
+ }
441
+ const matingBridgeSlot = /* @__PURE__ */ new Map();
442
+ {
443
+ const dipSlotByRow = /* @__PURE__ */ new Map();
444
+ for (const m of [...matings].sort((m1, m2) => m1.id - m2.id)) {
445
+ const [aId, bId] = partnersOf(m);
446
+ const a = individualById.get(aId);
447
+ const b = individualById.get(bId);
448
+ if (a === void 0 || b === void 0) continue;
449
+ if (a.generation !== b.generation) continue;
450
+ const row = rowOfGeneration.get(a.generation);
451
+ const loX = Math.min(placedById.get(aId).cx, placedById.get(bId).cx);
452
+ const hiX = Math.max(placedById.get(aId).cx, placedById.get(bId).cx);
453
+ const blocked = individuals.some(
454
+ (i) => i.id !== aId && i.id !== bId && rowOfGeneration.get(i.generation) === row && placedById.has(i.id) && placedById.get(i.id).cx > loX + 0.01 && placedById.get(i.id).cx < hiX - 0.01
455
+ );
456
+ if (!blocked) {
457
+ matingBridgeSlot.set(m.id, 0);
458
+ continue;
459
+ }
460
+ const slot = (dipSlotByRow.get(row) ?? 0) + 1;
461
+ dipSlotByRow.set(row, slot);
462
+ matingBridgeSlot.set(m.id, slot);
463
+ }
464
+ }
465
+ const rowDipBand = new Array(rowCount).fill(0);
466
+ for (const m of matings) {
467
+ const slot = matingBridgeSlot.get(m.id) ?? 0;
468
+ if (slot < 1) continue;
469
+ const [aId, bId] = partnersOf(m);
470
+ const a = individualById.get(aId);
471
+ const b = individualById.get(bId);
472
+ const gens = [a, b].filter((p) => p !== void 0).map((p) => p.generation);
473
+ if (gens.length === 0) continue;
474
+ const row = rowOfGeneration.get(Math.min(...gens));
475
+ rowDipBand[row] = Math.max(rowDipBand[row], BRIDGE_DIP_GAP + slot * BRIDGE_DIP_STEP + PED_LABEL_GAP);
476
+ }
477
+ const rowHeight = new Array(rowCount).fill(0);
478
+ for (const ind of individuals) {
479
+ const row = rowOfGeneration.get(ind.generation);
480
+ const lines = labelLinesOf.get(ind.id) ?? [];
481
+ const h = PED_GLYPH + PED_LABEL_GAP + rowDipBand[row] + (lines.length + 1) * PED_LABEL_LINE_H;
482
+ rowHeight[row] = Math.max(rowHeight[row], h);
483
+ }
484
+ const rowTop = [PADDING];
485
+ for (let r = 0; r < rowCount - 1; r++) {
486
+ rowTop.push(rowTop[r] + rowHeight[r] + CORRIDOR);
487
+ }
488
+ const glyphCyOfRow = (row) => rowTop[row] + PED_GLYPH / 2;
489
+ const addressByIndividual = /* @__PURE__ */ new Map();
490
+ for (let row = 0; row < rowCount; row++) {
491
+ const roman = romanNumeral(row + 1);
492
+ const inRow = individuals.filter((i) => rowOfGeneration.get(i.generation) === row).sort((a, b) => placedById.get(a.id).cx - placedById.get(b.id).cx || a.id - b.id);
493
+ inRow.forEach((ind, i) => addressByIndividual.set(ind.id, `${roman}-${i + 1}`));
494
+ }
495
+ const declaredConditionIds = new Set(input.conditions.map((c) => c.id));
496
+ const usedConditionIds = [...new Set(individuals.flatMap((i) => i.affectedBy))].filter((id) => declaredConditionIds.has(id)).sort((x, y) => x - y);
497
+ const conditionFills = usedConditionIds.map((id, i) => ({
498
+ id,
499
+ ink: PED_CONDITION_FILLS[i % PED_CONDITION_FILLS.length],
500
+ label: conditionLabelById.get(id) ?? `condition ${id}`
501
+ }));
502
+ const nodes = [];
503
+ for (const ind of individuals) {
504
+ const placed = placedById.get(ind.id);
505
+ const row = rowOfGeneration.get(ind.generation);
506
+ const cy = glyphCyOfRow(row);
507
+ const lines = labelLinesOf.get(ind.id) ?? [];
508
+ const address = addressByIndividual.get(ind.id);
509
+ nodes.push({
510
+ individualId: ind.id,
511
+ shape: shapeForSex(ind.sex),
512
+ cx: round(placed.cx),
513
+ cy: round(cy),
514
+ size: PED_GLYPH,
515
+ deceased: ind.deceased,
516
+ carrier: ind.carrier,
517
+ role: ind.role,
518
+ stillbirth: ind.lifeStatus === "stillbirth",
519
+ affectedBy: ind.affectedBy,
520
+ labelLines: lines,
521
+ // Labels sit BELOW the row's serial-union dip band (rowDipBand) so a hub's bridge dips
522
+ // never cross a label box.
523
+ labelTop: round(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
524
+ addressLabel: address,
525
+ title: individualTitle(ind, address, conditionLabelById, titleLabels)
526
+ });
527
+ }
528
+ const elements = [];
529
+ const cxOf = (id) => placedById.get(id).cx;
530
+ const cyOf = (id) => glyphCyOfRow(rowOfGeneration.get(individualById.get(id).generation));
531
+ const GLYPH_HALF = PED_GLYPH / 2;
532
+ const matingBarCenter = /* @__PURE__ */ new Map();
533
+ for (const s of sibships) {
534
+ const childIds = s.childIds.filter((id) => placedById.has(id));
535
+ if (childIds.length === 0) continue;
536
+ const xs = childIds.map(cxOf);
537
+ const center = (Math.min(...xs) + Math.max(...xs)) / 2;
538
+ matingBarCenter.set(s.matingId, center);
539
+ }
540
+ const matingMidpoint = /* @__PURE__ */ new Map();
541
+ const bridgeDipY = (slot) => BRIDGE_DIP_GAP + slot * BRIDGE_DIP_STEP;
542
+ for (const m of matings) {
543
+ const [aId, bId] = partnersOf(m);
544
+ const ax = cxOf(aId);
545
+ const bx = cxOf(bId);
546
+ const ay = cyOf(aId);
547
+ const by = cyOf(bId);
548
+ const leftId = ax <= bx ? aId : bId;
549
+ const rightId = ax <= bx ? bId : aId;
550
+ const lx = cxOf(leftId) + GLYPH_HALF;
551
+ const rx = cxOf(rightId) - GLYPH_HALF;
552
+ const title = m.consanguineous ? titleLabels.consanguineous : titleLabels.mating;
553
+ const bridgeSlot = matingBridgeSlot.get(m.id) ?? 0;
554
+ if (ay === by && bridgeSlot >= 1) {
555
+ const hubCx = cxOf(leftId);
556
+ const spouseCx = cxOf(rightId);
557
+ const glyphBottom = ay + GLYPH_HALF;
558
+ const dipY = glyphBottom + bridgeDipY(bridgeSlot);
559
+ const stubOff = Math.min(bridgeSlot * BRIDGE_STUB_X_STEP, GLYPH_HALF - 4);
560
+ const hubStubX = hubCx + stubOff;
561
+ const spouseStubX = spouseCx - stubOff;
562
+ elements.push({
563
+ edgeId: PED_MATING_ID_BASE + m.id,
564
+ kind: "mating-elbow",
565
+ points: [
566
+ { x: round(hubStubX), y: round(glyphBottom) },
567
+ { x: round(hubStubX), y: round(dipY) },
568
+ { x: round(spouseStubX), y: round(dipY) },
569
+ { x: round(spouseStubX), y: round(glyphBottom) }
570
+ ],
571
+ consanguineous: m.consanguineous,
572
+ title
573
+ });
574
+ matingMidpoint.set(m.id, { x: round((hubStubX + spouseStubX) / 2), y: round(dipY) });
575
+ } else if (ay === by) {
576
+ const y = ay;
577
+ elements.push({
578
+ edgeId: PED_MATING_ID_BASE + m.id,
579
+ kind: "mating",
580
+ points: [
581
+ { x: round(lx), y: round(y) },
582
+ { x: round(rx), y: round(y) }
583
+ ],
584
+ consanguineous: m.consanguineous,
585
+ title
586
+ });
587
+ const midX = (lx + rx) / 2;
588
+ const channelLeft = cxOf(leftId) + nodeHalfWidth(individualById.get(leftId));
589
+ const channelRight = cxOf(rightId) - nodeHalfWidth(individualById.get(rightId));
590
+ const barCenter = matingBarCenter.get(m.id);
591
+ const originX = barCenter !== void 0 && barCenter >= channelLeft && barCenter <= channelRight ? barCenter : midX;
592
+ matingMidpoint.set(m.id, { x: round(originX), y: round(y) });
593
+ } else {
594
+ const upId = ay <= by ? aId : bId;
595
+ const downId = ay <= by ? bId : aId;
596
+ const upX = cxOf(upId);
597
+ const upY = cyOf(upId);
598
+ const downX = cxOf(downId);
599
+ const downY = cyOf(downId);
600
+ if (childToSibship.has(downId)) {
601
+ const upRow = rowOfGeneration.get(individualById.get(upId).generation);
602
+ const laneY = rowTop[upRow] + rowHeight[upRow] + CORRIDOR / 4 + 4;
603
+ const downRight = downX >= upX;
604
+ const upExitX = upX + (downRight ? -1 : 1) * (nodeHalfWidth(individualById.get(upId)) + 6);
605
+ const downFarX = downX + (downRight ? 1 : -1) * (GLYPH_HALF + H_GAP / 2);
606
+ const downSideX = downX + (downRight ? 1 : -1) * GLYPH_HALF;
607
+ elements.push({
608
+ edgeId: PED_MATING_ID_BASE + m.id,
609
+ kind: "mating-elbow",
610
+ points: [
611
+ { x: round(upX + (downRight ? -GLYPH_HALF : GLYPH_HALF)), y: round(upY) },
612
+ { x: round(upExitX), y: round(upY) },
613
+ { x: round(upExitX), y: round(laneY) },
614
+ { x: round(downFarX), y: round(laneY) },
615
+ { x: round(downFarX), y: round(downY) },
616
+ { x: round(downSideX), y: round(downY) }
617
+ ],
618
+ consanguineous: m.consanguineous,
619
+ title
620
+ });
621
+ matingMidpoint.set(m.id, { x: round(downFarX), y: round(downY) });
622
+ } else {
623
+ const elbowX = (upX + downX) / 2;
624
+ elements.push({
625
+ edgeId: PED_MATING_ID_BASE + m.id,
626
+ kind: "mating-elbow",
627
+ points: [
628
+ { x: round(upX + (downX >= upX ? GLYPH_HALF : -GLYPH_HALF)), y: round(upY) },
629
+ { x: round(elbowX), y: round(upY) },
630
+ { x: round(elbowX), y: round(downY) },
631
+ { x: round(downX + (upX >= downX ? GLYPH_HALF : -GLYPH_HALF)), y: round(downY) }
632
+ ],
633
+ consanguineous: m.consanguineous,
634
+ title
635
+ });
636
+ matingMidpoint.set(m.id, { x: round(elbowX), y: round(downY) });
637
+ }
638
+ }
639
+ }
640
+ const twinZygositiesUsedSet = /* @__PURE__ */ new Set();
641
+ const unknownTwinJunctions = [];
642
+ const bendLaneByRow = /* @__PURE__ */ new Map();
643
+ for (const s of sibships) {
644
+ const childIds = s.childIds.filter((id) => individualById.has(id));
645
+ if (childIds.length === 0) continue;
646
+ const mid = matingMidpoint.get(s.matingId);
647
+ const childRow = rowOfGeneration.get(individualById.get(childIds[0]).generation);
648
+ const barY = rowTop[childRow] - CORRIDOR / 2;
649
+ const lane = bendLaneByRow.get(childRow) ?? 0;
650
+ bendLaneByRow.set(childRow, lane + 1);
651
+ const childXs = childIds.map(cxOf);
652
+ const barLeft = Math.min(...childXs);
653
+ const barRight = Math.max(...childXs);
654
+ if (mid !== void 0) {
655
+ const barCenterX = (barLeft + barRight) / 2;
656
+ const bendY = barY - 2 - lane % 3 * 3;
657
+ const descentPoints = Math.abs(mid.x - barCenterX) < 0.01 ? [
658
+ { x: round(mid.x), y: round(mid.y) },
659
+ { x: round(mid.x), y: round(barY) }
660
+ ] : [
661
+ { x: round(mid.x), y: round(mid.y) },
662
+ { x: round(mid.x), y: round(bendY) },
663
+ { x: round(barCenterX), y: round(bendY) },
664
+ { x: round(barCenterX), y: round(barY) }
665
+ ];
666
+ elements.push({
667
+ edgeId: PED_DESCENT_ID_BASE + s.id,
668
+ kind: "descent",
669
+ points: descentPoints,
670
+ consanguineous: false,
671
+ title: titleLabels.sibship
672
+ });
673
+ }
674
+ if (childIds.length > 1) {
675
+ elements.push({
676
+ edgeId: PED_SIBBAR_ID_BASE + s.id,
677
+ kind: "sibship-bar",
678
+ points: [
679
+ { x: round(barLeft), y: round(barY) },
680
+ { x: round(barRight), y: round(barY) }
681
+ ],
682
+ consanguineous: false,
683
+ title: titleLabels.sibship
684
+ });
685
+ }
686
+ const twinGroupOfChild = /* @__PURE__ */ new Map();
687
+ s.twinGroups.forEach((tg, ordinal) => {
688
+ const members = tg.childIds.filter((id) => childIds.includes(id));
689
+ for (const id of members) twinGroupOfChild.set(id, { ordinal, zygosity: tg.zygosity, members });
690
+ });
691
+ const emittedTwinOrdinals = /* @__PURE__ */ new Set();
692
+ for (const childId of childIds) {
693
+ const cx = cxOf(childId);
694
+ const childTop = cyOf(childId) - PED_GLYPH / 2;
695
+ const tg = twinGroupOfChild.get(childId);
696
+ if (tg === void 0) {
697
+ elements.push({
698
+ edgeId: PED_RISER_ID_BASE + childId,
699
+ kind: "riser",
700
+ points: [
701
+ { x: round(cx), y: round(barY) },
702
+ { x: round(cx), y: round(childTop) }
703
+ ],
704
+ consanguineous: false,
705
+ title: titleLabels.sibship
706
+ });
707
+ continue;
708
+ }
709
+ const memberXs = tg.members.map(cxOf);
710
+ const junctionX = (Math.min(...memberXs) + Math.max(...memberXs)) / 2;
711
+ const junctionY = barY + TWIN_JUNCTION_DROP;
712
+ twinZygositiesUsedSet.add(tg.zygosity);
713
+ if (!emittedTwinOrdinals.has(tg.ordinal)) {
714
+ emittedTwinOrdinals.add(tg.ordinal);
715
+ if (tg.zygosity === "unknown") {
716
+ unknownTwinJunctions.push({ x: round(junctionX), y: round(junctionY) });
717
+ }
718
+ elements.push({
719
+ edgeId: PED_RISER_ID_BASE + childId,
720
+ // anchored on the first member for a stable id
721
+ kind: "riser",
722
+ points: [
723
+ { x: round(junctionX), y: round(barY) },
724
+ { x: round(junctionX), y: round(junctionY) }
725
+ ],
726
+ consanguineous: false,
727
+ title: titleLabels.twins[tg.zygosity]
728
+ });
729
+ if (tg.zygosity === "mz" && memberXs.length >= 2) {
730
+ const tieY = junctionY + 6;
731
+ elements.push({
732
+ edgeId: PED_TWINBAR_ID_BASE + s.id * 100 + tg.ordinal,
733
+ kind: "twin-bar",
734
+ points: [
735
+ { x: round(Math.min(...memberXs)), y: round(tieY) },
736
+ { x: round(Math.max(...memberXs)), y: round(tieY) }
737
+ ],
738
+ consanguineous: false,
739
+ title: titleLabels.twins.mz
740
+ });
741
+ }
742
+ }
743
+ const horizontalThenDown = Math.abs(cx - junctionX) < 0.01 ? [
744
+ { x: round(cx), y: round(junctionY) },
745
+ { x: round(cx), y: round(childTop) }
746
+ ] : [
747
+ { x: round(junctionX), y: round(junctionY) },
748
+ { x: round(cx), y: round(junctionY) },
749
+ { x: round(cx), y: round(childTop) }
750
+ ];
751
+ elements.push({
752
+ edgeId: PED_RISER_ID_BASE + childId,
753
+ kind: "riser",
754
+ points: horizontalThenDown,
755
+ consanguineous: false,
756
+ title: titleLabels.twins[tg.zygosity]
757
+ });
758
+ }
759
+ }
760
+ let maxX = PADDING * 2;
761
+ let maxY = PADDING * 2;
762
+ for (const n of nodes) {
763
+ const labelW = n.labelLines.reduce((m, l) => Math.max(m, estimateTextWidth(l, PED_LABEL_FONT)), 0);
764
+ const half = Math.max(PED_GLYPH, labelW) / 2;
765
+ maxX = Math.max(maxX, n.cx + half + PADDING);
766
+ const labelBottom = n.labelTop + (n.labelLines.length + 1) * PED_LABEL_LINE_H;
767
+ maxY = Math.max(maxY, labelBottom + PADDING);
768
+ }
769
+ for (const el of elements) {
770
+ for (const p of el.points) {
771
+ maxX = Math.max(maxX, p.x + PADDING);
772
+ maxY = Math.max(maxY, p.y + PADDING);
773
+ }
774
+ }
775
+ const generations = [];
776
+ for (let row = 0; row < rowCount; row++) {
777
+ generations.push({ roman: romanNumeral(row + 1), y: round(glyphCyOfRow(row)) });
778
+ }
779
+ const ZYGOSITY_ORDER = ["mz", "dz", "unknown"];
780
+ const twinZygositiesUsed = ZYGOSITY_ORDER.filter((z) => twinZygositiesUsedSet.has(z));
781
+ return {
782
+ width: Math.ceil(maxX),
783
+ height: Math.ceil(maxY),
784
+ nodes,
785
+ elements,
786
+ generations,
787
+ conditionFills,
788
+ twinZygositiesUsed,
789
+ unknownTwinJunctions,
790
+ isolatedIndividualIds
791
+ };
792
+ }
793
+
794
+ // src/pedigree/svg.ts
795
+ var GLYPH_STROKE = "#52525b";
796
+ var LABEL_FILL = "#3f3f46";
797
+ var EDGE_INK = "#71717a";
798
+ var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="2"`;
799
+ var CONSANG_GAP = 3;
800
+ var ZYGOSITIES = ["mz", "dz", "unknown"];
801
+ var round2 = (n) => Math.round(n * 100) / 100;
802
+ function glyphOutline(shape, cx, cy, half) {
803
+ if (shape === "square") {
804
+ return `<rect x="${round2(cx - half)}" y="${round2(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS}/>`;
805
+ }
806
+ if (shape === "circle") {
807
+ return `<circle cx="${cx}" cy="${cy}" r="${half}" ${GLYPH_ATTRS}/>`;
808
+ }
809
+ return `<polygon points="${cx},${round2(cy - half)} ${round2(cx + half)},${cy} ${cx},${round2(cy + half)} ${round2(cx - half)},${cy}" ${GLYPH_ATTRS}/>`;
810
+ }
811
+ function glyphVerticalExtentAt(shape, cy, half, dx) {
812
+ const ax = Math.min(Math.abs(dx), half);
813
+ if (shape === "square") return [cy - half, cy + half];
814
+ if (shape === "circle") {
815
+ const h2 = Math.sqrt(Math.max(0, half * half - ax * ax));
816
+ return [cy - h2, cy + h2];
817
+ }
818
+ const h = half - ax;
819
+ return [cy - h, cy + h];
820
+ }
821
+ function fillPartitions(n, half, inkByCondition) {
822
+ const ids = n.affectedBy;
823
+ if (ids.length === 0) return "";
824
+ const cx = n.cx;
825
+ const cy = n.cy;
826
+ const sliceW = half * 2 / ids.length;
827
+ return ids.map((id, i) => {
828
+ const left = cx - half + i * sliceW;
829
+ const SAMPLES = 8;
830
+ const top = [];
831
+ const bottom = [];
832
+ for (let s = 0; s <= SAMPLES; s++) {
833
+ const x = left + sliceW * s / SAMPLES;
834
+ const [yt, yb] = glyphVerticalExtentAt(n.shape, cy, half, x - cx);
835
+ top.push(`${round2(x)},${round2(yt)}`);
836
+ bottom.push(`${round2(x)},${round2(yb)}`);
837
+ }
838
+ const pts = [...top, ...bottom.reverse()].join(" ");
839
+ const ink = inkByCondition.get(id) ?? GLYPH_STROKE;
840
+ return `<polygon points="${pts}" fill="${ink}" stroke="none"/>`;
841
+ }).join("");
842
+ }
843
+ function carrierDot(n) {
844
+ if (!n.carrier || n.affectedBy.length > 0) return "";
845
+ return `<circle cx="${n.cx}" cy="${n.cy}" r="4" fill="${GLYPH_STROKE}" stroke="none"/>`;
846
+ }
847
+ function deceasedSlash(n, half) {
848
+ if (!n.deceased) return "";
849
+ const ext = half + 4;
850
+ return `<line x1="${round2(n.cx - ext)}" y1="${round2(n.cy - ext)}" x2="${round2(n.cx + ext)}" y2="${round2(n.cy + ext)}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`;
851
+ }
852
+ function probandArrow(n, half) {
853
+ if (n.role === null) return "";
854
+ const tipX = round2(n.cx - half);
855
+ const tipY = round2(n.cy + half);
856
+ const tailX = round2(tipX - 16);
857
+ const tailY = round2(tipY + 16);
858
+ const filled = n.role === "proband";
859
+ const fill = filled ? GLYPH_STROKE : "transparent";
860
+ const shaft = `<line x1="${tailX}" y1="${tailY}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`;
861
+ const head = `<polygon points="${tipX},${tipY} ${round2(tipX - 8)},${round2(tipY + 2)} ${round2(tipX - 2)},${round2(tipY + 8)}" fill="${fill}" stroke="${GLYPH_STROKE}" stroke-width="1.5"/>`;
862
+ return shaft + head;
863
+ }
864
+ function stillbirthMark(n, half) {
865
+ if (!n.stillbirth) return "";
866
+ const y = round2(n.cy + half + PED_ADDRESS_FONT);
867
+ return `<text x="${round2(n.cx - half - 2)}" y="${y}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" font-weight="bold" fill="${LABEL_FILL}">SB</text>`;
868
+ }
869
+ function nodeSvg(n, inkByCondition) {
870
+ const half = n.size / 2;
871
+ const pieces = [
872
+ `<title>${xmlEscape(n.title)}</title>`,
873
+ // Fill partitions sit UNDER the outline so the border stays crisp.
874
+ fillPartitions(n, half, inkByCondition),
875
+ glyphOutline(n.shape, n.cx, n.cy, half),
876
+ carrierDot(n),
877
+ deceasedSlash(n, half),
878
+ probandArrow(n, half),
879
+ stillbirthMark(n, half)
880
+ ];
881
+ const addressY = round2(n.labelTop + PED_ADDRESS_FONT);
882
+ pieces.push(
883
+ `<text x="${n.cx}" y="${addressY}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" fill="${LABEL_FILL}">${xmlEscape(n.addressLabel)}</text>`
884
+ );
885
+ if (n.labelLines.length > 0) {
886
+ const firstBaseline = round2(n.labelTop + PED_LABEL_LINE_H + 10);
887
+ const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round2(firstBaseline + i * PED_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
888
+ pieces.push(
889
+ `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" fill="${LABEL_FILL}">${tspans}</text>`
890
+ );
891
+ }
892
+ return `<g data-individual-id="p${n.individualId}">${pieces.join("")}</g>`;
893
+ }
894
+ function pathData(points) {
895
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
896
+ }
897
+ function elementSvg(el) {
898
+ const pts = el.points;
899
+ const title = `<title>${xmlEscape(el.title)}</title>`;
900
+ const stroke = `stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"`;
901
+ const draw = (offsetY) => {
902
+ const shifted = pts.map((p) => ({ x: p.x, y: round2(p.y + offsetY) }));
903
+ if (shifted.length === 2) {
904
+ return `<line x1="${shifted[0].x}" y1="${shifted[0].y}" x2="${shifted[1].x}" y2="${shifted[1].y}" ${stroke}/>`;
905
+ }
906
+ return `<path d="${pathData(shifted)}" fill="none" ${stroke}/>`;
907
+ };
908
+ const body = el.consanguineous && (el.kind === "mating" || el.kind === "mating-elbow") ? draw(-CONSANG_GAP / 2) + draw(CONSANG_GAP / 2) : draw(0);
909
+ return `<g data-edge-id="${el.edgeId}">${title}${body}</g>`;
910
+ }
911
+ function unknownTwinMarks(layout) {
912
+ return layout.unknownTwinJunctions.map(
913
+ (p) => `<text x="${round2(p.x + 6)}" y="${round2(p.y + 4)}" font-family="${FONT_FAMILY}" font-size="12" font-weight="bold" fill="${LABEL_FILL}">?</text>`
914
+ ).join("");
915
+ }
916
+ var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="1.5"`;
917
+ function miniShapeSwatch(shape, x, y) {
918
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
919
+ if (shape === "square") return `<rect x="${round2(cx - 6)}" y="${round2(y - 6)}" width="12" height="12" ${MINI_ATTRS}/>`;
920
+ if (shape === "circle") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/>`;
921
+ return `<polygon points="${cx},${round2(y - 7)} ${round2(cx + 7)},${y} ${cx},${round2(y + 7)} ${round2(cx - 7)},${y}" ${MINI_ATTRS}/>`;
922
+ }
923
+ function miniSwatchCircle(filled, ink, x, y) {
924
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
925
+ return `<circle cx="${cx}" cy="${y}" r="6" fill="${filled ? ink : "transparent"}" stroke="${GLYPH_STROKE}" stroke-width="1.5"/>`;
926
+ }
927
+ function miniCarrierSwatch(x, y) {
928
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
929
+ return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/><circle cx="${cx}" cy="${y}" r="2" fill="${GLYPH_STROKE}" stroke="none"/>`;
930
+ }
931
+ function miniDeceasedSwatch(x, y) {
932
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
933
+ return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/><line x1="${round2(cx - 7)}" y1="${round2(y - 7)}" x2="${round2(cx + 7)}" y2="${round2(y + 7)}" stroke="${GLYPH_STROKE}" stroke-width="1.5"/>`;
934
+ }
935
+ function miniArrowSwatch(filled, x, y) {
936
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
937
+ const tipX = round2(cx - 2);
938
+ const tipY = round2(y + 2);
939
+ return `<line x1="${round2(tipX - 8)}" y1="${round2(tipY + 8)}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE}" stroke-width="1.5"/><polygon points="${tipX},${tipY} ${round2(tipX - 5)},${round2(tipY + 1)} ${round2(tipX - 1)},${round2(tipY + 5)}" fill="${filled ? GLYPH_STROKE : "transparent"}" stroke="${GLYPH_STROKE}" stroke-width="1"/>`;
940
+ }
941
+ function miniConsanguineousSwatch(x, y) {
942
+ const x1 = round2(x + 2);
943
+ const x2 = round2(x + LEGEND_SWATCH_W - 2);
944
+ return `<line x1="${x1}" y1="${round2(y - 1.5)}" x2="${x2}" y2="${round2(y - 1.5)}" stroke="${EDGE_INK}" stroke-width="1.5"/><line x1="${x1}" y1="${round2(y + 1.5)}" x2="${x2}" y2="${round2(y + 1.5)}" stroke="${EDGE_INK}" stroke-width="1.5"/>`;
945
+ }
946
+ function miniTwinSwatch(zygosity, x, y) {
947
+ const cx = round2(x + LEGEND_SWATCH_W / 2);
948
+ const apexY = round2(y - 6);
949
+ const baseY = round2(y + 6);
950
+ const left = round2(cx - 6);
951
+ const right = round2(cx + 6);
952
+ const stub = `<line x1="${cx}" y1="${apexY}" x2="${cx}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${right}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${left}" y2="${baseY}" stroke="${EDGE_INK}" stroke-width="1.5"/><line x1="${right}" y1="${y}" x2="${right}" y2="${baseY}" stroke="${EDGE_INK}" stroke-width="1.5"/>`;
953
+ if (zygosity === "mz") {
954
+ return stub + `<line x1="${left}" y1="${round2(y + 3)}" x2="${right}" y2="${round2(y + 3)}" stroke="${EDGE_INK}" stroke-width="1.5"/>`;
955
+ }
956
+ if (zygosity === "unknown") {
957
+ return stub + `<text x="${round2(cx + 8)}" y="${round2(y + 3)}" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL}">?</text>`;
958
+ }
959
+ return stub;
960
+ }
961
+ function pedigreeLayoutSvg(layout, opts = {}) {
962
+ const labels = opts.labels ?? PEDIGREE_SVG_LABELS_EN;
963
+ const inkByCondition = new Map(layout.conditionFills.map((c) => [c.id, c.ink]));
964
+ const parts = [];
965
+ for (const el of layout.elements) parts.push(elementSvg(el));
966
+ parts.push(unknownTwinMarks(layout));
967
+ for (const n of layout.nodes) parts.push(nodeSvg(n, inkByCondition));
968
+ for (const g of layout.generations) {
969
+ parts.push(
970
+ `<text x="${round2(16)}" y="${round2(g.y + PED_LABEL_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" font-weight="bold" fill="${LABEL_FILL}">${xmlEscape(g.roman)}</text>`
971
+ );
972
+ }
973
+ let width = layout.width;
974
+ let height = layout.height;
975
+ if (opts.legend !== false && layout.nodes.length > 0) {
976
+ const entries = [];
977
+ const shapesUsed = new Set(layout.nodes.map((n) => n.shape));
978
+ for (const shape of ["square", "circle", "diamond"]) {
979
+ if (!shapesUsed.has(shape)) continue;
980
+ entries.push({ swatch: (x, y) => miniShapeSwatch(shape, x, y), label: labels.shapes[shape] });
981
+ }
982
+ for (const c of layout.conditionFills) {
983
+ entries.push({ swatch: (x, y) => miniSwatchCircle(true, c.ink, x, y), label: c.label });
984
+ }
985
+ if (layout.nodes.some((n) => n.affectedBy.length === 0)) {
986
+ entries.push({ swatch: (x, y) => miniSwatchCircle(false, GLYPH_STROKE, x, y), label: labels.unaffected });
987
+ }
988
+ if (layout.nodes.some((n) => n.carrier && n.affectedBy.length === 0)) {
989
+ entries.push({ swatch: miniCarrierSwatch, label: labels.carrier });
990
+ }
991
+ if (layout.nodes.some((n) => n.deceased)) {
992
+ entries.push({ swatch: miniDeceasedSwatch, label: labels.deceased });
993
+ }
994
+ if (layout.nodes.some((n) => n.role === "proband")) {
995
+ entries.push({ swatch: (x, y) => miniArrowSwatch(true, x, y), label: labels.proband });
996
+ }
997
+ if (layout.nodes.some((n) => n.role === "consultand")) {
998
+ entries.push({ swatch: (x, y) => miniArrowSwatch(false, x, y), label: labels.consultand });
999
+ }
1000
+ if (layout.nodes.some((n) => n.stillbirth)) {
1001
+ entries.push({ swatch: () => "", label: labels.stillbirth });
1002
+ }
1003
+ if (layout.elements.some((el) => el.consanguineous)) {
1004
+ entries.push({ swatch: miniConsanguineousSwatch, label: labels.consanguineous });
1005
+ }
1006
+ const twinsUsed = new Set(layout.twinZygositiesUsed);
1007
+ for (const z of ZYGOSITIES) {
1008
+ if (!twinsUsed.has(z)) continue;
1009
+ entries.push({ swatch: (x, y) => miniTwinSwatch(z, x, y), label: labels.twins[z] });
1010
+ }
1011
+ if (layout.isolatedIndividualIds.length > 0) {
1012
+ entries.push({ swatch: () => "", label: labels.isolated });
1013
+ }
1014
+ const block = legendBlock(entries, layout.height);
1015
+ if (block.svg !== "") {
1016
+ parts.push(block.svg);
1017
+ width = Math.max(width, block.width);
1018
+ height = block.height;
1019
+ }
1020
+ }
1021
+ const w = Math.ceil(width);
1022
+ const h = Math.ceil(height);
1023
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
1024
+ }
1025
+
1026
+ // src/pedigree/render.ts
1027
+ function pedigreeSvg(input, opts = {}) {
1028
+ const layout = computePedigreeLayout(input, {
1029
+ ...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
1030
+ ...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
1031
+ });
1032
+ const svg = pedigreeLayoutSvg(layout, {
1033
+ ...opts.legend === false ? { legend: false } : {},
1034
+ ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
1035
+ });
1036
+ return { svg, layout };
1037
+ }
1038
+
1039
+ export { LIFE_STATUSES, MAX_CONDITIONS_PER_INDIVIDUAL, PEDIGREE_SVG_LABELS_EN, PEDIGREE_TITLE_LABELS_EN, PED_ADDRESS_FONT, PED_CONDITION_FILLS, PED_DESCENT_ID_BASE, PED_GLYPH, PED_LABEL_FONT, PED_LABEL_GAP, PED_LABEL_LINE_H, PED_MATING_ID_BASE, PED_RISER_ID_BASE, PED_SIBBAR_ID_BASE, PED_TWINBAR_ID_BASE, PedigreeValidationError, computePedigreeLayout, pedigreeIssues, pedigreeLayoutSvg, pedigreeSvg, validatePedigree };
1040
+ //# sourceMappingURL=chunk-F47C6ZEB.js.map
1041
+ //# sourceMappingURL=chunk-F47C6ZEB.js.map