schematex 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.
Files changed (181) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +379 -0
  3. package/dist/chunk-2MQWZ2XY.cjs +453 -0
  4. package/dist/chunk-2MQWZ2XY.cjs.map +1 -0
  5. package/dist/chunk-2UKC6ZCY.cjs +1803 -0
  6. package/dist/chunk-2UKC6ZCY.cjs.map +1 -0
  7. package/dist/chunk-34X3ZJ6E.cjs +783 -0
  8. package/dist/chunk-34X3ZJ6E.cjs.map +1 -0
  9. package/dist/chunk-3FTUWAXK.cjs +1220 -0
  10. package/dist/chunk-3FTUWAXK.cjs.map +1 -0
  11. package/dist/chunk-3J7TFUOC.js +745 -0
  12. package/dist/chunk-3J7TFUOC.js.map +1 -0
  13. package/dist/chunk-47ZC6EMJ.js +1009 -0
  14. package/dist/chunk-47ZC6EMJ.js.map +1 -0
  15. package/dist/chunk-4DBRNOPA.cjs +750 -0
  16. package/dist/chunk-4DBRNOPA.cjs.map +1 -0
  17. package/dist/chunk-4G7ZIBHN.js +778 -0
  18. package/dist/chunk-4G7ZIBHN.js.map +1 -0
  19. package/dist/chunk-5C7DPDHQ.js +1321 -0
  20. package/dist/chunk-5C7DPDHQ.js.map +1 -0
  21. package/dist/chunk-ADOXGKAK.js +1251 -0
  22. package/dist/chunk-ADOXGKAK.js.map +1 -0
  23. package/dist/chunk-BE5HNDA5.cjs +874 -0
  24. package/dist/chunk-BE5HNDA5.cjs.map +1 -0
  25. package/dist/chunk-CZRM7LT7.js +889 -0
  26. package/dist/chunk-CZRM7LT7.js.map +1 -0
  27. package/dist/chunk-D4JTSPOL.js +1795 -0
  28. package/dist/chunk-D4JTSPOL.js.map +1 -0
  29. package/dist/chunk-DS47NTWZ.cjs +1034 -0
  30. package/dist/chunk-DS47NTWZ.cjs.map +1 -0
  31. package/dist/chunk-FDLZEKEB.js +449 -0
  32. package/dist/chunk-FDLZEKEB.js.map +1 -0
  33. package/dist/chunk-FGPTCDUT.cjs +1851 -0
  34. package/dist/chunk-FGPTCDUT.cjs.map +1 -0
  35. package/dist/chunk-HDKDQAEQ.cjs +86 -0
  36. package/dist/chunk-HDKDQAEQ.cjs.map +1 -0
  37. package/dist/chunk-IX554O5K.js +346 -0
  38. package/dist/chunk-IX554O5K.js.map +1 -0
  39. package/dist/chunk-KLJEK547.js +71 -0
  40. package/dist/chunk-KLJEK547.js.map +1 -0
  41. package/dist/chunk-LMFSHK45.js +1028 -0
  42. package/dist/chunk-LMFSHK45.js.map +1 -0
  43. package/dist/chunk-MDICUK6F.cjs +1258 -0
  44. package/dist/chunk-MDICUK6F.cjs.map +1 -0
  45. package/dist/chunk-N7KOXOMX.cjs +363 -0
  46. package/dist/chunk-N7KOXOMX.cjs.map +1 -0
  47. package/dist/chunk-NYCIK4SU.cjs +775 -0
  48. package/dist/chunk-NYCIK4SU.cjs.map +1 -0
  49. package/dist/chunk-PDPHRZZT.js +770 -0
  50. package/dist/chunk-PDPHRZZT.js.map +1 -0
  51. package/dist/chunk-ROFLJ74T.js +1212 -0
  52. package/dist/chunk-ROFLJ74T.js.map +1 -0
  53. package/dist/chunk-S6BK5DB6.cjs +845 -0
  54. package/dist/chunk-S6BK5DB6.cjs.map +1 -0
  55. package/dist/chunk-U4I37IBN.js +874 -0
  56. package/dist/chunk-U4I37IBN.js.map +1 -0
  57. package/dist/chunk-U5GGE6PJ.js +839 -0
  58. package/dist/chunk-U5GGE6PJ.js.map +1 -0
  59. package/dist/chunk-UHLYS3W5.cjs +1015 -0
  60. package/dist/chunk-UHLYS3W5.cjs.map +1 -0
  61. package/dist/chunk-URSKIHSY.cjs +881 -0
  62. package/dist/chunk-URSKIHSY.cjs.map +1 -0
  63. package/dist/chunk-V6WO7RK7.cjs +1056 -0
  64. package/dist/chunk-V6WO7RK7.cjs.map +1 -0
  65. package/dist/chunk-VFQCTXOX.js +869 -0
  66. package/dist/chunk-VFQCTXOX.js.map +1 -0
  67. package/dist/chunk-XQ52ICHU.cjs +895 -0
  68. package/dist/chunk-XQ52ICHU.cjs.map +1 -0
  69. package/dist/chunk-XX4BKS7Y.js +1051 -0
  70. package/dist/chunk-XX4BKS7Y.js.map +1 -0
  71. package/dist/chunk-XXU36667.js +1844 -0
  72. package/dist/chunk-XXU36667.js.map +1 -0
  73. package/dist/chunk-ZX7QKZK2.cjs +1326 -0
  74. package/dist/chunk-ZX7QKZK2.cjs.map +1 -0
  75. package/dist/diagrams/blockdiagram/index.cjs +25 -0
  76. package/dist/diagrams/blockdiagram/index.cjs.map +1 -0
  77. package/dist/diagrams/blockdiagram/index.d.cts +67 -0
  78. package/dist/diagrams/blockdiagram/index.d.ts +67 -0
  79. package/dist/diagrams/blockdiagram/index.js +4 -0
  80. package/dist/diagrams/blockdiagram/index.js.map +1 -0
  81. package/dist/diagrams/circuit/index.cjs +34 -0
  82. package/dist/diagrams/circuit/index.cjs.map +1 -0
  83. package/dist/diagrams/circuit/index.d.cts +138 -0
  84. package/dist/diagrams/circuit/index.d.ts +138 -0
  85. package/dist/diagrams/circuit/index.js +5 -0
  86. package/dist/diagrams/circuit/index.js.map +1 -0
  87. package/dist/diagrams/ecomap/index.cjs +30 -0
  88. package/dist/diagrams/ecomap/index.cjs.map +1 -0
  89. package/dist/diagrams/ecomap/index.d.cts +15 -0
  90. package/dist/diagrams/ecomap/index.d.ts +15 -0
  91. package/dist/diagrams/ecomap/index.js +5 -0
  92. package/dist/diagrams/ecomap/index.js.map +1 -0
  93. package/dist/diagrams/entity/index.cjs +26 -0
  94. package/dist/diagrams/entity/index.cjs.map +1 -0
  95. package/dist/diagrams/entity/index.d.cts +54 -0
  96. package/dist/diagrams/entity/index.d.ts +54 -0
  97. package/dist/diagrams/entity/index.js +5 -0
  98. package/dist/diagrams/entity/index.js.map +1 -0
  99. package/dist/diagrams/fishbone/index.cjs +34 -0
  100. package/dist/diagrams/fishbone/index.cjs.map +1 -0
  101. package/dist/diagrams/fishbone/index.d.cts +185 -0
  102. package/dist/diagrams/fishbone/index.d.ts +185 -0
  103. package/dist/diagrams/fishbone/index.js +5 -0
  104. package/dist/diagrams/fishbone/index.js.map +1 -0
  105. package/dist/diagrams/flowchart/index.cjs +34 -0
  106. package/dist/diagrams/flowchart/index.cjs.map +1 -0
  107. package/dist/diagrams/flowchart/index.d.cts +2 -0
  108. package/dist/diagrams/flowchart/index.d.ts +2 -0
  109. package/dist/diagrams/flowchart/index.js +5 -0
  110. package/dist/diagrams/flowchart/index.js.map +1 -0
  111. package/dist/diagrams/genogram/index.cjs +38 -0
  112. package/dist/diagrams/genogram/index.cjs.map +1 -0
  113. package/dist/diagrams/genogram/index.d.cts +20 -0
  114. package/dist/diagrams/genogram/index.d.ts +20 -0
  115. package/dist/diagrams/genogram/index.js +5 -0
  116. package/dist/diagrams/genogram/index.js.map +1 -0
  117. package/dist/diagrams/ladder/index.cjs +26 -0
  118. package/dist/diagrams/ladder/index.cjs.map +1 -0
  119. package/dist/diagrams/ladder/index.d.cts +49 -0
  120. package/dist/diagrams/ladder/index.d.ts +49 -0
  121. package/dist/diagrams/ladder/index.js +5 -0
  122. package/dist/diagrams/ladder/index.js.map +1 -0
  123. package/dist/diagrams/logic/index.cjs +26 -0
  124. package/dist/diagrams/logic/index.cjs.map +1 -0
  125. package/dist/diagrams/logic/index.d.cts +73 -0
  126. package/dist/diagrams/logic/index.d.ts +73 -0
  127. package/dist/diagrams/logic/index.js +5 -0
  128. package/dist/diagrams/logic/index.js.map +1 -0
  129. package/dist/diagrams/orgchart/index.cjs +30 -0
  130. package/dist/diagrams/orgchart/index.cjs.map +1 -0
  131. package/dist/diagrams/orgchart/index.d.cts +100 -0
  132. package/dist/diagrams/orgchart/index.d.ts +100 -0
  133. package/dist/diagrams/orgchart/index.js +5 -0
  134. package/dist/diagrams/orgchart/index.js.map +1 -0
  135. package/dist/diagrams/pedigree/index.cjs +30 -0
  136. package/dist/diagrams/pedigree/index.cjs.map +1 -0
  137. package/dist/diagrams/pedigree/index.d.cts +15 -0
  138. package/dist/diagrams/pedigree/index.d.ts +15 -0
  139. package/dist/diagrams/pedigree/index.js +5 -0
  140. package/dist/diagrams/pedigree/index.js.map +1 -0
  141. package/dist/diagrams/phylo/index.cjs +30 -0
  142. package/dist/diagrams/phylo/index.cjs.map +1 -0
  143. package/dist/diagrams/phylo/index.d.cts +32 -0
  144. package/dist/diagrams/phylo/index.d.ts +32 -0
  145. package/dist/diagrams/phylo/index.js +5 -0
  146. package/dist/diagrams/phylo/index.js.map +1 -0
  147. package/dist/diagrams/sld/index.cjs +26 -0
  148. package/dist/diagrams/sld/index.cjs.map +1 -0
  149. package/dist/diagrams/sld/index.d.cts +58 -0
  150. package/dist/diagrams/sld/index.d.ts +58 -0
  151. package/dist/diagrams/sld/index.js +5 -0
  152. package/dist/diagrams/sld/index.js.map +1 -0
  153. package/dist/diagrams/sociogram/index.cjs +26 -0
  154. package/dist/diagrams/sociogram/index.cjs.map +1 -0
  155. package/dist/diagrams/sociogram/index.d.cts +76 -0
  156. package/dist/diagrams/sociogram/index.d.ts +76 -0
  157. package/dist/diagrams/sociogram/index.js +5 -0
  158. package/dist/diagrams/sociogram/index.js.map +1 -0
  159. package/dist/diagrams/timing/index.cjs +21 -0
  160. package/dist/diagrams/timing/index.cjs.map +1 -0
  161. package/dist/diagrams/timing/index.d.cts +9 -0
  162. package/dist/diagrams/timing/index.d.ts +9 -0
  163. package/dist/diagrams/timing/index.js +4 -0
  164. package/dist/diagrams/timing/index.js.map +1 -0
  165. package/dist/diagrams/venn/index.cjs +38 -0
  166. package/dist/diagrams/venn/index.cjs.map +1 -0
  167. package/dist/diagrams/venn/index.d.cts +69 -0
  168. package/dist/diagrams/venn/index.d.ts +69 -0
  169. package/dist/diagrams/venn/index.js +5 -0
  170. package/dist/diagrams/venn/index.js.map +1 -0
  171. package/dist/index-BSlza1YY.d.ts +150 -0
  172. package/dist/index-BXefHVce.d.cts +150 -0
  173. package/dist/index.cjs +2033 -0
  174. package/dist/index.cjs.map +1 -0
  175. package/dist/index.d.cts +29 -0
  176. package/dist/index.d.ts +29 -0
  177. package/dist/index.js +1944 -0
  178. package/dist/index.js.map +1 -0
  179. package/dist/types-DqfcYkcY.d.cts +741 -0
  180. package/dist/types-DqfcYkcY.d.ts +741 -0
  181. package/package.json +163 -0
@@ -0,0 +1,1028 @@
1
+ import { resolvePersonTheme, STROKE_WIDTH, cssCustomProperties } from './chunk-IX554O5K.js';
2
+ import { title, desc, svgRoot, el, rect, path, defs, line, group, text, polygon, circle } from './chunk-KLJEK547.js';
3
+
4
+ // src/diagrams/pedigree/parser.ts
5
+ var PedigreeParseError = class extends Error {
6
+ constructor(message, line2) {
7
+ super(`Line ${line2}: ${message}`);
8
+ this.line = line2;
9
+ this.name = "PedigreeParseError";
10
+ }
11
+ line;
12
+ };
13
+ var COUPLE_OPS = [
14
+ { token: "-/-", type: "separated" },
15
+ { token: "==", type: "consanguineous" },
16
+ { token: "--", type: "married" },
17
+ { token: "~", type: "cohabiting" }
18
+ ];
19
+ var VALID_SEX = /* @__PURE__ */ new Set(["male", "female", "unknown", "amab", "afab", "uaab"]);
20
+ var GENETIC_STATUSES = /* @__PURE__ */ new Set([
21
+ "affected",
22
+ "carrier",
23
+ "carrier-x",
24
+ "obligate-carrier",
25
+ "presymptomatic",
26
+ "unaffected"
27
+ ]);
28
+ var MARKERS = /* @__PURE__ */ new Set(["proband", "consultand", "evaluated"]);
29
+ var VALID_STATUS = /* @__PURE__ */ new Set(["deceased", "stillborn", "pregnancy", "sab", "tab", "ectopic"]);
30
+ function parsePedigree(text2) {
31
+ const rawLines = text2.split("\n");
32
+ let i = 0;
33
+ while (i < rawLines.length && rawLines[i].trim() === "") i++;
34
+ if (i >= rawLines.length) throw new PedigreeParseError("Empty input", 1);
35
+ const headerLine = rawLines[i].trim();
36
+ const headerMatch = headerLine.match(/^pedigree\s*(?:"([^"]*)")?$/i);
37
+ if (!headerMatch) throw new PedigreeParseError("Expected 'pedigree' header", i + 1);
38
+ const metadata = {};
39
+ if (headerMatch[1]) metadata.title = headerMatch[1];
40
+ i++;
41
+ const legend = [];
42
+ const individualsMap = /* @__PURE__ */ new Map();
43
+ const relationships = [];
44
+ while (i < rawLines.length) {
45
+ const raw = rawLines[i];
46
+ const trimmed = raw.trim();
47
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("//")) {
48
+ i++;
49
+ continue;
50
+ }
51
+ const legendMatch = trimmed.match(
52
+ /^legend:\s*([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"\s*(?:\(\s*fill:\s*([a-zA-Z-]+)\s*\))?$/
53
+ );
54
+ if (legendMatch) {
55
+ legend.push({
56
+ id: legendMatch[1],
57
+ label: legendMatch[2],
58
+ fill: legendMatch[3] ?? "full"
59
+ });
60
+ i++;
61
+ continue;
62
+ }
63
+ const coupleMatch = detectCoupleOp(trimmed);
64
+ if (coupleMatch) {
65
+ const { leftId, op, rightRaw } = coupleMatch;
66
+ const lineNum = i + 1;
67
+ const { id: rightId, propsStr: rightProps } = parseIdWithProps(rightRaw);
68
+ const leftKey = leftId.toLowerCase();
69
+ const rightKey = rightId.toLowerCase();
70
+ if (!individualsMap.has(leftKey)) {
71
+ throw new PedigreeParseError(`Unknown individual '${leftId}'`, lineNum);
72
+ }
73
+ if (rightProps) {
74
+ individualsMap.set(rightKey, buildIndividual(rightId, rightProps));
75
+ } else if (!individualsMap.has(rightKey)) {
76
+ throw new PedigreeParseError(`Unknown individual '${rightId}'`, lineNum);
77
+ }
78
+ relationships.push({ type: op.type, from: leftKey, to: rightKey });
79
+ const coupleIndent = leadingSpaces(raw);
80
+ i++;
81
+ while (i < rawLines.length) {
82
+ const childLine = rawLines[i];
83
+ const childTrimmed = childLine.trim();
84
+ if (childTrimmed === "" || childTrimmed.startsWith("#")) {
85
+ i++;
86
+ continue;
87
+ }
88
+ if (leadingSpaces(childLine) <= coupleIndent) break;
89
+ const { id: childId, propsStr: propsStr2 } = parseIdWithProps(childTrimmed);
90
+ const childKey = childId.toLowerCase();
91
+ individualsMap.set(childKey, buildIndividual(childId, propsStr2));
92
+ const coupleKey = `${leftKey}+${rightKey}`;
93
+ relationships.push({ type: "parent-child", from: coupleKey, to: childKey });
94
+ i++;
95
+ }
96
+ continue;
97
+ }
98
+ const { id, propsStr } = parseIdWithProps(trimmed);
99
+ const key = id.toLowerCase();
100
+ const ind = buildIndividual(id, propsStr);
101
+ const existing = individualsMap.get(key);
102
+ if (existing) {
103
+ individualsMap.set(key, mergeIndividual(existing, ind));
104
+ } else {
105
+ individualsMap.set(key, ind);
106
+ }
107
+ i++;
108
+ }
109
+ return {
110
+ type: "pedigree",
111
+ individuals: Array.from(individualsMap.values()),
112
+ relationships,
113
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
114
+ legend: legend.length > 0 ? legend : void 0
115
+ };
116
+ }
117
+ function leadingSpaces(line2) {
118
+ const m = line2.match(/^(\s*)/);
119
+ return m ? m[1].length : 0;
120
+ }
121
+ function detectCoupleOp(trimmed) {
122
+ for (const op of COUPLE_OPS) {
123
+ let bracketDepth = 0;
124
+ for (let j = 0; j < trimmed.length; j++) {
125
+ if (trimmed[j] === "[") bracketDepth++;
126
+ if (trimmed[j] === "]") bracketDepth--;
127
+ if (bracketDepth > 0) continue;
128
+ if (trimmed.substring(j, j + op.token.length) === op.token) {
129
+ const left = trimmed.substring(0, j).trim();
130
+ const right = trimmed.substring(j + op.token.length).trim();
131
+ if (left && right && /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(left)) {
132
+ return { leftId: left, op, rightRaw: right };
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ function parseIdWithProps(raw) {
140
+ const bracketIdx = raw.indexOf("[");
141
+ if (bracketIdx === -1) return { id: raw.trim(), propsStr: null };
142
+ const id = raw.substring(0, bracketIdx).trim();
143
+ const endBracket = raw.lastIndexOf("]");
144
+ const propsStr = raw.substring(bracketIdx + 1, endBracket === -1 ? raw.length : endBracket);
145
+ return { id, propsStr };
146
+ }
147
+ function splitProps(propsStr) {
148
+ const result = [];
149
+ let current = "";
150
+ let parenDepth = 0;
151
+ for (const ch of propsStr) {
152
+ if (ch === "(") parenDepth++;
153
+ if (ch === ")") parenDepth--;
154
+ if (ch === "," && parenDepth === 0) {
155
+ result.push(current);
156
+ current = "";
157
+ } else {
158
+ current += ch;
159
+ }
160
+ }
161
+ if (current.trim()) result.push(current);
162
+ return result;
163
+ }
164
+ function buildIndividual(id, propsStr, _lineNum) {
165
+ const ind = {
166
+ id: id.toLowerCase(),
167
+ label: id,
168
+ sex: "unknown",
169
+ status: "alive"
170
+ };
171
+ if (!propsStr) return ind;
172
+ const tokens = splitProps(propsStr);
173
+ for (const rawToken of tokens) {
174
+ const token = rawToken.trim();
175
+ const lower = token.toLowerCase();
176
+ if (VALID_SEX.has(lower)) {
177
+ ind.sex = lower === "amab" ? "male" : lower === "afab" ? "female" : lower === "uaab" ? "unknown" : lower;
178
+ } else if (VALID_STATUS.has(lower)) {
179
+ ind.status = lower;
180
+ } else if (GENETIC_STATUSES.has(lower)) {
181
+ ind.geneticStatus = lower;
182
+ } else if (MARKERS.has(lower)) {
183
+ if (!ind.markers) ind.markers = [];
184
+ ind.markers.push(lower);
185
+ } else if (/^\d{4}$/.test(lower)) {
186
+ ind.birthYear = parseInt(token, 10);
187
+ } else if (lower.startsWith("affected:")) {
188
+ ind.geneticStatus = "affected";
189
+ const traits = token.substring("affected:".length).trim().split("+").map((t) => t.trim());
190
+ ind.conditions = traits.map((t) => ({ label: t, fill: "full" }));
191
+ } else if (token.includes(":")) {
192
+ const colonIdx = token.indexOf(":");
193
+ const key = token.substring(0, colonIdx).trim().toLowerCase();
194
+ const value = token.substring(colonIdx + 1).trim().replace(/^"|"$/g, "");
195
+ if (key === "label") {
196
+ ind.label = value;
197
+ } else {
198
+ if (!ind.properties) ind.properties = {};
199
+ ind.properties[key] = value;
200
+ }
201
+ }
202
+ }
203
+ return ind;
204
+ }
205
+ function mergeIndividual(existing, incoming) {
206
+ return {
207
+ ...existing,
208
+ sex: incoming.sex !== "unknown" ? incoming.sex : existing.sex,
209
+ status: incoming.status !== "alive" ? incoming.status : existing.status,
210
+ birthYear: incoming.birthYear ?? existing.birthYear,
211
+ geneticStatus: incoming.geneticStatus ?? existing.geneticStatus,
212
+ markers: incoming.markers ?? existing.markers,
213
+ conditions: incoming.conditions ?? existing.conditions,
214
+ properties: { ...existing.properties, ...incoming.properties }
215
+ };
216
+ }
217
+
218
+ // src/diagrams/pedigree/layout.ts
219
+ var LABEL_HEIGHT = 20;
220
+ var LABEL_GAP = 6;
221
+ var GEN_LABEL_MARGIN = 50;
222
+ function layoutPedigree(ast, config) {
223
+ const graph = buildGraph(ast);
224
+ assignGenerations(graph);
225
+ const ordered = orderNodesInGenerations(graph);
226
+ const positions = assignPositions(ordered, graph, config);
227
+ const edges = computeEdges(graph, positions, config);
228
+ return packageResult(positions, edges, graph, config);
229
+ }
230
+ function buildGraph(ast) {
231
+ const individuals = /* @__PURE__ */ new Map();
232
+ for (const ind of ast.individuals) individuals.set(ind.id, ind);
233
+ const familyUnits = [];
234
+ const childOf = /* @__PURE__ */ new Map();
235
+ const coupleRels = ast.relationships.filter(
236
+ (r) => r.type === "married" || r.type === "separated" || r.type === "consanguineous" || r.type === "cohabiting"
237
+ );
238
+ for (const rel of coupleRels) {
239
+ const fuId = `${rel.from}+${rel.to}`;
240
+ const indA = individuals.get(rel.from);
241
+ const indB = individuals.get(rel.to);
242
+ let partners;
243
+ if (indA && indB) {
244
+ if (indA.sex === "male" && indB.sex === "female") {
245
+ partners = [rel.from, rel.to];
246
+ } else if (indA.sex === "female" && indB.sex === "male") {
247
+ partners = [rel.to, rel.from];
248
+ } else {
249
+ partners = [rel.from, rel.to];
250
+ }
251
+ } else {
252
+ partners = [rel.from, rel.to];
253
+ }
254
+ const children = [];
255
+ for (const r of ast.relationships) {
256
+ if (r.type === "parent-child" && r.from === fuId) {
257
+ children.push(r.to);
258
+ childOf.set(r.to, fuId);
259
+ }
260
+ }
261
+ children.sort((a, b) => {
262
+ const ia = individuals.get(a);
263
+ const ib = individuals.get(b);
264
+ return (ia?.birthYear ?? 9999) - (ib?.birthYear ?? 9999);
265
+ });
266
+ familyUnits.push({ id: fuId, partners, relationship: rel.type, children });
267
+ }
268
+ return { individuals, familyUnits, generations: /* @__PURE__ */ new Map(), childOf };
269
+ }
270
+ function assignGenerations(graph) {
271
+ const { individuals, familyUnits, childOf, generations } = graph;
272
+ const allIds = Array.from(individuals.keys());
273
+ const roots = allIds.filter((id) => !childOf.has(id));
274
+ if (roots.length === 0 && allIds.length > 0) {
275
+ for (const id of allIds) generations.set(id, 0);
276
+ return;
277
+ }
278
+ for (const root of roots) {
279
+ if (!generations.has(root)) generations.set(root, 0);
280
+ }
281
+ let changed = true;
282
+ while (changed) {
283
+ changed = false;
284
+ for (const fu of familyUnits) {
285
+ const gen0 = generations.get(fu.partners[0]);
286
+ const gen1 = generations.get(fu.partners[1]);
287
+ let partnerGen;
288
+ if (gen0 !== void 0 && gen1 !== void 0) {
289
+ partnerGen = Math.max(gen0, gen1);
290
+ if (gen0 !== partnerGen) {
291
+ generations.set(fu.partners[0], partnerGen);
292
+ changed = true;
293
+ }
294
+ if (gen1 !== partnerGen) {
295
+ generations.set(fu.partners[1], partnerGen);
296
+ changed = true;
297
+ }
298
+ } else if (gen0 !== void 0) {
299
+ partnerGen = gen0;
300
+ generations.set(fu.partners[1], partnerGen);
301
+ changed = true;
302
+ } else if (gen1 !== void 0) {
303
+ partnerGen = gen1;
304
+ generations.set(fu.partners[0], partnerGen);
305
+ changed = true;
306
+ } else {
307
+ continue;
308
+ }
309
+ for (const childId of fu.children) {
310
+ const childGen = partnerGen + 1;
311
+ const existing = generations.get(childId);
312
+ if (existing === void 0 || existing < childGen) {
313
+ generations.set(childId, childGen);
314
+ changed = true;
315
+ }
316
+ }
317
+ }
318
+ }
319
+ for (const id of allIds) {
320
+ if (!generations.has(id)) generations.set(id, 0);
321
+ }
322
+ }
323
+ function orderNodesInGenerations(graph) {
324
+ const genGroups = /* @__PURE__ */ new Map();
325
+ for (const [id, gen] of graph.generations) {
326
+ const grp = genGroups.get(gen) ?? [];
327
+ grp.push(id);
328
+ genGroups.set(gen, grp);
329
+ }
330
+ const genIndices = Array.from(genGroups.keys()).sort((a, b) => a - b);
331
+ const result = [];
332
+ for (const genIdx of genIndices) {
333
+ const nodeIds = genGroups.get(genIdx) ?? [];
334
+ result.push({ index: genIdx, nodeIds: orderGeneration(nodeIds, graph) });
335
+ }
336
+ return result;
337
+ }
338
+ function orderGeneration(nodeIds, graph) {
339
+ if (nodeIds.length <= 1) return [...nodeIds];
340
+ const nodeSet = new Set(nodeIds);
341
+ const placed = /* @__PURE__ */ new Set();
342
+ const ordered = [];
343
+ const fuInGen = graph.familyUnits.filter(
344
+ (fu) => nodeSet.has(fu.partners[0]) || nodeSet.has(fu.partners[1])
345
+ );
346
+ for (const fu of fuInGen) {
347
+ for (const p of fu.partners) {
348
+ if (nodeSet.has(p) && !placed.has(p)) {
349
+ ordered.push(p);
350
+ placed.add(p);
351
+ }
352
+ }
353
+ }
354
+ for (const id of nodeIds) {
355
+ if (!placed.has(id)) {
356
+ ordered.push(id);
357
+ placed.add(id);
358
+ }
359
+ }
360
+ return ordered;
361
+ }
362
+ function assignPositions(orderedGens, graph, config) {
363
+ const positions = /* @__PURE__ */ new Map();
364
+ const { nodeWidth, nodeSpacingX } = config;
365
+ const half = nodeWidth / 2;
366
+ const coupleGap = nodeWidth + nodeSpacingX * 0.6;
367
+ const familyGap = nodeWidth + nodeSpacingX * 1.5;
368
+ const genStepY = config.nodeHeight + LABEL_HEIGHT + LABEL_GAP + config.nodeSpacingY;
369
+ for (const gen of orderedGens) {
370
+ const y = gen.index * genStepY + half;
371
+ const segments = buildSegments(gen.nodeIds, graph);
372
+ let xCursor = GEN_LABEL_MARGIN + half;
373
+ for (let s = 0; s < segments.length; s++) {
374
+ const seg = segments[s];
375
+ if (s > 0) xCursor += familyGap;
376
+ if (seg.type === "couple") {
377
+ positions.set(seg.ids[0], { id: seg.ids[0], x: xCursor, y, generation: gen.index });
378
+ xCursor += coupleGap;
379
+ positions.set(seg.ids[1], { id: seg.ids[1], x: xCursor, y, generation: gen.index });
380
+ } else {
381
+ positions.set(seg.ids[0], { id: seg.ids[0], x: xCursor, y, generation: gen.index });
382
+ }
383
+ }
384
+ }
385
+ resolveOverlaps(positions, orderedGens, config);
386
+ centerChildrenUnderParents(positions, graph, config);
387
+ resolveOverlaps(positions, orderedGens, config);
388
+ return positions;
389
+ }
390
+ function buildSegments(nodeIds, graph) {
391
+ const nodeSet = new Set(nodeIds);
392
+ const segments = [];
393
+ const placed = /* @__PURE__ */ new Set();
394
+ for (const id of nodeIds) {
395
+ if (placed.has(id)) continue;
396
+ const fu = graph.familyUnits.find(
397
+ (f) => (f.partners[0] === id || f.partners[1] === id) && !placed.has(f.partners[0]) && !placed.has(f.partners[1]) && nodeSet.has(f.partners[0]) && nodeSet.has(f.partners[1])
398
+ );
399
+ if (fu) {
400
+ segments.push({ type: "couple", ids: [fu.partners[0], fu.partners[1]] });
401
+ placed.add(fu.partners[0]);
402
+ placed.add(fu.partners[1]);
403
+ } else {
404
+ segments.push({ type: "single", ids: [id] });
405
+ placed.add(id);
406
+ }
407
+ }
408
+ return segments;
409
+ }
410
+ function centerChildrenUnderParents(positions, graph, config) {
411
+ const coupleGap = config.nodeSpacingX + config.nodeWidth;
412
+ const personToFUs = /* @__PURE__ */ new Map();
413
+ for (const fu of graph.familyUnits) {
414
+ for (const p of fu.partners) {
415
+ const arr = personToFUs.get(p) ?? [];
416
+ arr.push(fu);
417
+ personToFUs.set(p, arr);
418
+ }
419
+ }
420
+ for (let pass = 0; pass < 3; pass++) {
421
+ for (const fu of graph.familyUnits) {
422
+ if (fu.children.length === 0) continue;
423
+ const posA = positions.get(fu.partners[0]);
424
+ const posB = positions.get(fu.partners[1]);
425
+ if (!posA || !posB) continue;
426
+ const parentMidX = (posA.x + posB.x) / 2;
427
+ const childSpacing = config.nodeSpacingX + config.nodeWidth;
428
+ const sortedChildren = [...fu.children].sort((a, b) => {
429
+ const ia = graph.individuals.get(a);
430
+ const ib = graph.individuals.get(b);
431
+ return (ia?.birthYear ?? 9999) - (ib?.birthYear ?? 9999);
432
+ });
433
+ if (sortedChildren.length > 1) {
434
+ const gaps = [];
435
+ for (let j = 0; j < sortedChildren.length - 1; j++) {
436
+ let gap = childSpacing;
437
+ const nextChild = sortedChildren[j + 1];
438
+ const nextFUs = personToFUs.get(nextChild) ?? [];
439
+ for (const cfu of nextFUs) {
440
+ const pid = cfu.partners[0] === nextChild ? cfu.partners[1] : cfu.partners[0];
441
+ const cInd = graph.individuals.get(nextChild);
442
+ const pInd = graph.individuals.get(pid);
443
+ if (!(cInd?.sex === "male" || cInd?.sex !== "female" && pInd?.sex === "female")) {
444
+ gap += coupleGap;
445
+ }
446
+ }
447
+ const currChild = sortedChildren[j];
448
+ const currFUs = personToFUs.get(currChild) ?? [];
449
+ for (const cfu of currFUs) {
450
+ const pid = cfu.partners[0] === currChild ? cfu.partners[1] : cfu.partners[0];
451
+ const cInd = graph.individuals.get(currChild);
452
+ const pInd = graph.individuals.get(pid);
453
+ if (cInd?.sex === "male" || cInd?.sex !== "female" && pInd?.sex === "female") {
454
+ gap += coupleGap;
455
+ }
456
+ }
457
+ gaps.push(gap);
458
+ }
459
+ const totalWidth = gaps.reduce((s, g) => s + g, 0);
460
+ const startX = parentMidX - totalWidth / 2;
461
+ let cx = startX;
462
+ for (let j = 0; j < sortedChildren.length; j++) {
463
+ const pos = positions.get(sortedChildren[j]);
464
+ if (pos) pos.x = cx;
465
+ if (j < gaps.length) cx += gaps[j];
466
+ }
467
+ } else {
468
+ const pos = positions.get(sortedChildren[0]);
469
+ if (pos) pos.x = parentMidX;
470
+ }
471
+ for (const childId of fu.children) {
472
+ const childPos = positions.get(childId);
473
+ if (!childPos) continue;
474
+ const childFUs = personToFUs.get(childId) ?? [];
475
+ for (const childFU of childFUs) {
476
+ const partnerId = childFU.partners[0] === childId ? childFU.partners[1] : childFU.partners[0];
477
+ const partnerPos = positions.get(partnerId);
478
+ if (!partnerPos) continue;
479
+ const childInd = graph.individuals.get(childId);
480
+ const partnerInd = graph.individuals.get(partnerId);
481
+ if (childInd?.sex === "male" || childInd?.sex !== "female" && partnerInd?.sex === "female") {
482
+ partnerPos.x = childPos.x + coupleGap;
483
+ } else {
484
+ partnerPos.x = childPos.x - coupleGap;
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ function resolveOverlaps(positions, orderedGens, config) {
492
+ const minGap = config.nodeWidth + config.nodeSpacingX;
493
+ for (const gen of orderedGens) {
494
+ const genNodes = gen.nodeIds.map((id) => positions.get(id)).filter((p) => p !== void 0);
495
+ genNodes.sort((a, b) => a.x - b.x);
496
+ for (let j = 1; j < genNodes.length; j++) {
497
+ const gap = genNodes[j].x - genNodes[j - 1].x;
498
+ if (gap < minGap) {
499
+ const shift = minGap - gap;
500
+ for (let k = j; k < genNodes.length; k++) genNodes[k].x += shift;
501
+ }
502
+ }
503
+ }
504
+ let minX = Infinity;
505
+ for (const pos of positions.values()) {
506
+ if (pos.x < minX) minX = pos.x;
507
+ }
508
+ const minAllowed = GEN_LABEL_MARGIN + config.nodeWidth / 2;
509
+ if (minX < minAllowed) {
510
+ const shift = minAllowed - minX;
511
+ for (const pos of positions.values()) pos.x += shift;
512
+ }
513
+ }
514
+ function computeEdges(graph, positions, config) {
515
+ const edges = [];
516
+ const half = config.nodeWidth / 2;
517
+ const dropY_offset = config.nodeHeight / 2 + LABEL_HEIGHT + LABEL_GAP + config.nodeSpacingY * 0.35;
518
+ for (const fu of graph.familyUnits) {
519
+ const posA = positions.get(fu.partners[0]);
520
+ const posB = positions.get(fu.partners[1]);
521
+ if (!posA || !posB) continue;
522
+ const leftPos = posA.x < posB.x ? posA : posB;
523
+ const rightPos = posA.x < posB.x ? posB : posA;
524
+ const leftId = posA.x < posB.x ? fu.partners[0] : fu.partners[1];
525
+ const rightId = posA.x < posB.x ? fu.partners[1] : fu.partners[0];
526
+ const coupleRel = { type: fu.relationship, from: leftId, to: rightId };
527
+ const couplePath = `M ${leftPos.x + half} ${leftPos.y} L ${rightPos.x - half} ${rightPos.y}`;
528
+ edges.push({ from: leftId, to: rightId, relationship: coupleRel, path: couplePath });
529
+ if (fu.relationship === "consanguineous") {
530
+ const offset = 3;
531
+ const consPath = `M ${leftPos.x + half} ${leftPos.y + offset} L ${rightPos.x - half} ${rightPos.y + offset}`;
532
+ edges.push({
533
+ from: leftId,
534
+ to: rightId,
535
+ relationship: { type: "consanguineous", from: leftId, to: rightId, label: "_double" },
536
+ path: consPath
537
+ });
538
+ }
539
+ if (fu.children.length > 0) {
540
+ const midX = (posA.x + posB.x) / 2;
541
+ const coupleY = posA.y;
542
+ const dropY = coupleY + dropY_offset;
543
+ const childPositions = fu.children.map((cid) => ({ id: cid, pos: positions.get(cid) })).filter((c) => c.pos !== void 0);
544
+ if (childPositions.length === 0) continue;
545
+ childPositions.sort((a, b) => a.pos.x - b.pos.x);
546
+ const leftX = childPositions[0].pos.x;
547
+ const rightX = childPositions[childPositions.length - 1].pos.x;
548
+ edges.push({
549
+ from: fu.partners[0],
550
+ to: fu.partners[1],
551
+ relationship: { type: "parent-child", from: fu.id, to: "_drop" },
552
+ path: `M ${midX} ${coupleY} L ${midX} ${dropY}`
553
+ });
554
+ if (childPositions.length > 1) {
555
+ edges.push({
556
+ from: fu.partners[0],
557
+ to: fu.partners[1],
558
+ relationship: { type: "parent-child", from: fu.id, to: "_sibship" },
559
+ path: `M ${leftX} ${dropY} L ${rightX} ${dropY}`
560
+ });
561
+ }
562
+ for (const child of childPositions) {
563
+ const childTop = child.pos.y - config.nodeHeight / 2;
564
+ const childPath = childPositions.length === 1 ? `M ${midX} ${coupleY} L ${midX} ${childTop}` : `M ${child.pos.x} ${dropY} L ${child.pos.x} ${childTop}`;
565
+ edges.push({
566
+ from: fu.id,
567
+ to: child.id,
568
+ relationship: { type: "parent-child", from: fu.id, to: child.id },
569
+ path: childPath
570
+ });
571
+ }
572
+ }
573
+ }
574
+ return edges;
575
+ }
576
+ function packageResult(positions, edges, graph, config) {
577
+ const padding = 40;
578
+ const nodes = [];
579
+ for (const [id, pos] of positions) {
580
+ const ind = graph.individuals.get(id);
581
+ if (!ind) continue;
582
+ nodes.push({
583
+ id,
584
+ x: pos.x - config.nodeWidth / 2 + padding,
585
+ y: pos.y - config.nodeHeight / 2 + padding,
586
+ width: config.nodeWidth,
587
+ height: config.nodeHeight,
588
+ generation: pos.generation,
589
+ individual: ind
590
+ });
591
+ }
592
+ const shiftedEdges = edges.map((e) => ({
593
+ ...e,
594
+ path: shiftPath(e.path, padding, padding)
595
+ }));
596
+ let maxX = 0;
597
+ let maxY = 0;
598
+ let minX = Infinity;
599
+ for (const node of nodes) {
600
+ const cx = node.x + node.width / 2;
601
+ const labelText = node.individual.label || node.individual.id;
602
+ const labelHalfWidth = labelText.length * 3.8;
603
+ const right = Math.max(node.x + node.width, cx + labelHalfWidth);
604
+ const left = Math.min(node.x, cx - labelHalfWidth);
605
+ const bottom = node.y + node.height;
606
+ if (right > maxX) maxX = right;
607
+ if (bottom > maxY) maxY = bottom;
608
+ if (left < minX) minX = left;
609
+ }
610
+ if (minX < 0) {
611
+ const shift = -minX;
612
+ for (const node of nodes) {
613
+ node.x += shift;
614
+ }
615
+ for (const edge of shiftedEdges) {
616
+ edge.path = shiftPath(edge.path, shift, 0);
617
+ }
618
+ maxX += shift;
619
+ }
620
+ return {
621
+ width: maxX + padding,
622
+ height: maxY + padding + LABEL_GAP + LABEL_HEIGHT + 10,
623
+ nodes,
624
+ edges: shiftedEdges
625
+ };
626
+ }
627
+ function shiftPath(pathData, dx, dy) {
628
+ return pathData.replace(
629
+ /([ML])\s*([\d.-]+)\s+([\d.-]+)/g,
630
+ (_match, cmd, xStr, yStr) => {
631
+ const x = parseFloat(xStr) + dx;
632
+ const y = parseFloat(yStr) + dy;
633
+ return `${cmd} ${x} ${y}`;
634
+ }
635
+ );
636
+ }
637
+
638
+ // src/diagrams/pedigree/renderer.ts
639
+ function renderPedigree(layout, config, ast) {
640
+ const t = resolvePersonTheme(config.theme);
641
+ const defsStr = buildDefs(layout.nodes, t);
642
+ const styleStr = buildStyles(config, t);
643
+ const genGroups = groupByGeneration(layout.nodes);
644
+ const edgeLayers = renderEdges(layout.edges);
645
+ const nodeLayers = renderNodes(genGroups);
646
+ const labelLayer = renderLabels(layout.nodes, genGroups, config);
647
+ const genLabels = renderGenerationLabels(genGroups);
648
+ const nodeCount = layout.nodes.length;
649
+ const genCount = genGroups.size;
650
+ const diagramTitle = ast?.metadata?.title ?? "Pedigree";
651
+ const children = [
652
+ title(diagramTitle),
653
+ desc(`Pedigree chart with ${nodeCount} individuals across ${genCount} generations`),
654
+ defsStr,
655
+ styleStr,
656
+ genLabels,
657
+ edgeLayers,
658
+ ...nodeLayers,
659
+ labelLayer
660
+ ];
661
+ if (ast?.legend && ast.legend.length > 0) {
662
+ children.push(renderLegend(ast.legend, layout, config, t));
663
+ }
664
+ return svgRoot(
665
+ {
666
+ viewBox: `0 0 ${layout.width} ${layout.height}`,
667
+ class: "schematex-diagram schematex-pedigree",
668
+ width: layout.width,
669
+ height: layout.height
670
+ },
671
+ children
672
+ );
673
+ }
674
+ function buildDefs(nodes, t) {
675
+ const children = [];
676
+ const needs = /* @__PURE__ */ new Set();
677
+ for (const n of nodes) {
678
+ const gs = n.individual.geneticStatus;
679
+ if (gs) needs.add(gs);
680
+ }
681
+ if (needs.has("carrier")) {
682
+ children.push(
683
+ el("clipPath", { id: "schematex-pedigree-clip-carrier-rect" }, [
684
+ rect({ x: "0", y: "0", width: "50%", height: "100%" })
685
+ ]),
686
+ el("clipPath", { id: "schematex-pedigree-clip-carrier-circle" }, [
687
+ rect({ x: "-50", y: "-50", width: "50", height: "100" })
688
+ ])
689
+ );
690
+ }
691
+ children.push(
692
+ el("marker", {
693
+ id: "schematex-pedigree-proband-arrow",
694
+ viewBox: "0 0 10 10",
695
+ refX: "0",
696
+ refY: "5",
697
+ markerWidth: "8",
698
+ markerHeight: "8",
699
+ orient: "auto-start-reverse"
700
+ }, [
701
+ path({ d: "M 0 0 L 10 5 L 0 10 z", fill: t.stroke })
702
+ ])
703
+ );
704
+ return defs(children);
705
+ }
706
+ function buildStyles(config, t) {
707
+ const css = `
708
+ .schematex-pedigree {${cssCustomProperties(t)}
709
+ background: ${t.bg};
710
+ }
711
+ .schematex-pedigree-shape { fill: ${t.fill}; stroke: ${t.stroke}; stroke-width: ${STROKE_WIDTH.normal}; stroke-linejoin: round; }
712
+ .schematex-pedigree-label { font-family: ${config.fontFamily}; font-size: ${config.fontSize}px; text-anchor: middle; fill: ${t.text}; }
713
+ .schematex-pedigree-gen-label { font-family: ${config.fontFamily}; font-size: 14px; font-weight: bold; fill: ${t.text}; text-anchor: middle; }
714
+ .schematex-pedigree-edge { stroke: ${t.stroke}; stroke-width: ${STROKE_WIDTH.normal}; fill: none; stroke-linecap: round; stroke-linejoin: round; }
715
+ .schematex-pedigree-deceased-mark { stroke: ${t.deceasedMark}; stroke-width: ${STROKE_WIDTH.normal}; stroke-linecap: round; }
716
+ .schematex-pedigree-affected-fill { fill: ${t.conditionFill}; }
717
+ .schematex-pedigree-carrier-fill { fill: ${t.conditionFill}; }
718
+ .schematex-pedigree-carrier-x-dot { fill: ${t.conditionFill}; }
719
+ .schematex-pedigree-presymptomatic-mark { stroke: ${t.conditionFill}; stroke-width: ${STROKE_WIDTH.normal}; }
720
+ .schematex-pedigree-proband-arrow-line { stroke: ${t.stroke}; stroke-width: ${STROKE_WIDTH.normal}; fill: none; marker-end: url(#schematex-pedigree-proband-arrow); }
721
+ .schematex-pedigree-proband-label { font-family: ${config.fontFamily}; font-size: 10px; font-weight: bold; fill: ${t.stroke}; }
722
+ .schematex-pedigree-legend { font-family: ${config.fontFamily}; font-size: 11px; fill: ${t.text}; }
723
+ .schematex-pedigree-legend-box { fill: ${t.fill}; stroke: ${t.neutral}; stroke-width: 1; }
724
+ `;
725
+ return el("style", {}, css);
726
+ }
727
+ function renderEdges(edges) {
728
+ const children = [];
729
+ for (const edge of edges) {
730
+ const relType = edge.relationship.type;
731
+ const cssClass = `schematex-pedigree-edge schematex-pedigree-edge-${relType}`;
732
+ const elements = [
733
+ el("path", { d: edge.path, class: "schematex-pedigree-edge-path" })
734
+ ];
735
+ if (relType === "separated") {
736
+ const mid = pathMidpoint(edge.path);
737
+ if (mid) {
738
+ elements.push(
739
+ line({
740
+ x1: mid.x - 4,
741
+ y1: mid.y - 6,
742
+ x2: mid.x + 4,
743
+ y2: mid.y + 6,
744
+ class: "schematex-pedigree-edge"
745
+ })
746
+ );
747
+ }
748
+ }
749
+ children.push(
750
+ group({ class: cssClass, "data-from": edge.from, "data-to": edge.to }, elements)
751
+ );
752
+ }
753
+ return group({ class: "schematex-pedigree-edges" }, children);
754
+ }
755
+ function pathMidpoint(pathData) {
756
+ const coords = pathData.match(/[\d.-]+/g);
757
+ if (!coords || coords.length < 4) return null;
758
+ return {
759
+ x: (parseFloat(coords[0]) + parseFloat(coords[2])) / 2,
760
+ y: (parseFloat(coords[1]) + parseFloat(coords[3])) / 2
761
+ };
762
+ }
763
+ function groupByGeneration(nodes) {
764
+ const groups = /* @__PURE__ */ new Map();
765
+ for (const node of nodes) {
766
+ const grp = groups.get(node.generation) ?? [];
767
+ grp.push(node);
768
+ groups.set(node.generation, grp);
769
+ }
770
+ return groups;
771
+ }
772
+ function renderNodes(genGroups) {
773
+ const layers = [];
774
+ const sortedGens = Array.from(genGroups.keys()).sort((a, b) => a - b);
775
+ for (const genIdx of sortedGens) {
776
+ const nodes = genGroups.get(genIdx) ?? [];
777
+ const nodeElements = [];
778
+ for (const node of nodes) {
779
+ const cx = node.x + node.width / 2;
780
+ const cy = node.y + node.height / 2;
781
+ nodeElements.push(renderPedigreeSymbol(node.individual, cx, cy, node.width));
782
+ }
783
+ layers.push(
784
+ group(
785
+ { class: `schematex-pedigree-generation schematex-pedigree-generation-${genIdx}`, "data-generation": genIdx },
786
+ nodeElements
787
+ )
788
+ );
789
+ }
790
+ return layers;
791
+ }
792
+ function renderPedigreeSymbol(ind, x, y, size) {
793
+ const half = size / 2;
794
+ const classes = ["schematex-pedigree-node", `schematex-pedigree-${ind.sex === "other" ? "unknown" : ind.sex}`];
795
+ if (ind.status === "deceased") classes.push("schematex-pedigree-deceased");
796
+ if (ind.geneticStatus) classes.push(`schematex-pedigree-${ind.geneticStatus}`);
797
+ const titleText = formatTitle(ind);
798
+ const children = [title(titleText)];
799
+ children.push(baseShape(ind.sex, half));
800
+ const gs = ind.geneticStatus;
801
+ if (gs === "affected") {
802
+ children.push(affectedFill(ind.sex, half));
803
+ } else if (gs === "carrier") {
804
+ children.push(carrierFill(ind.sex, half));
805
+ } else if (gs === "carrier-x" || gs === "obligate-carrier") {
806
+ children.push(carrierDot(half));
807
+ }
808
+ if (gs === "presymptomatic") {
809
+ children.push(
810
+ line({ x1: 0, y1: -half, x2: 0, y2: half, class: "schematex-pedigree-presymptomatic-mark" })
811
+ );
812
+ }
813
+ if (ind.status === "deceased") {
814
+ const ext = ind.sex === "female" ? half * 0.707 : half;
815
+ children.push(
816
+ line({ x1: ext, y1: -ext, x2: -ext, y2: ext, class: "schematex-pedigree-deceased-mark" })
817
+ );
818
+ }
819
+ if (ind.markers?.includes("proband")) {
820
+ const arrowLen = 20;
821
+ children.push(
822
+ line({
823
+ x1: -half - arrowLen,
824
+ y1: half + arrowLen,
825
+ x2: -half - 2,
826
+ y2: half + 2,
827
+ class: "schematex-pedigree-proband-arrow-line"
828
+ }),
829
+ text(
830
+ { x: -half - arrowLen - 4, y: half + arrowLen + 4, class: "schematex-pedigree-proband-label", "text-anchor": "end" },
831
+ "P"
832
+ )
833
+ );
834
+ }
835
+ if (ind.markers?.includes("consultand")) {
836
+ const arrowLen = 20;
837
+ children.push(
838
+ line({
839
+ x1: -half - arrowLen,
840
+ y1: half + arrowLen,
841
+ x2: -half - 2,
842
+ y2: half + 2,
843
+ class: "schematex-pedigree-proband-arrow-line"
844
+ }),
845
+ text(
846
+ { x: -half - arrowLen - 4, y: half + arrowLen + 4, class: "schematex-pedigree-proband-label", "text-anchor": "end" },
847
+ "C"
848
+ )
849
+ );
850
+ }
851
+ if (ind.markers?.includes("evaluated")) {
852
+ children.push(
853
+ text(
854
+ { x: 0, y: -half - 4, class: "schematex-pedigree-proband-label", "text-anchor": "middle" },
855
+ "E"
856
+ )
857
+ );
858
+ }
859
+ return group(
860
+ {
861
+ class: classes.join(" "),
862
+ "data-individual-id": ind.id,
863
+ transform: `translate(${x}, ${y})`
864
+ },
865
+ children
866
+ );
867
+ }
868
+ function baseShape(sex, half) {
869
+ switch (sex) {
870
+ case "male":
871
+ return rect({ x: -half, y: -half, width: half * 2, height: half * 2, class: "schematex-pedigree-shape" });
872
+ case "female":
873
+ return circle({ cx: 0, cy: 0, r: half, class: "schematex-pedigree-shape" });
874
+ default:
875
+ return polygon({ points: `0,${-half} ${half},0 0,${half} ${-half},0`, class: "schematex-pedigree-shape" });
876
+ }
877
+ }
878
+ function affectedFill(sex, half) {
879
+ const attrs = { class: "schematex-pedigree-affected-fill" };
880
+ switch (sex) {
881
+ case "male":
882
+ return rect({ x: -half, y: -half, width: half * 2, height: half * 2, ...attrs });
883
+ case "female":
884
+ return circle({ cx: 0, cy: 0, r: half, ...attrs });
885
+ default:
886
+ return polygon({ points: `0,${-half} ${half},0 0,${half} ${-half},0`, ...attrs });
887
+ }
888
+ }
889
+ function carrierFill(sex, half) {
890
+ const clipSuffix = sex === "female" ? "circle" : "rect";
891
+ const attrs = {
892
+ class: "schematex-pedigree-carrier-fill",
893
+ "clip-path": `url(#schematex-pedigree-clip-carrier-${clipSuffix})`
894
+ };
895
+ switch (sex) {
896
+ case "male":
897
+ return rect({ x: -half, y: -half, width: half * 2, height: half * 2, ...attrs });
898
+ case "female":
899
+ return circle({ cx: 0, cy: 0, r: half, ...attrs });
900
+ default:
901
+ return polygon({ points: `0,${-half} ${half},0 0,${half} ${-half},0`, ...attrs });
902
+ }
903
+ }
904
+ function carrierDot(half) {
905
+ return circle({ cx: 0, cy: 0, r: half * 0.15, class: "schematex-pedigree-carrier-x-dot" });
906
+ }
907
+ function formatTitle(ind) {
908
+ const name = ind.label.charAt(0).toUpperCase() + ind.label.slice(1);
909
+ if (ind.geneticStatus && ind.geneticStatus !== "unaffected") {
910
+ return `${name} (${ind.geneticStatus})`;
911
+ }
912
+ return name;
913
+ }
914
+ function renderLabels(nodes, genGroups, config) {
915
+ const labels = [];
916
+ const genNumbering = /* @__PURE__ */ new Map();
917
+ const sortedGens = Array.from(genGroups.keys()).sort((a, b) => a - b);
918
+ for (const genIdx of sortedGens) {
919
+ const genNodes = genGroups.get(genIdx) ?? [];
920
+ const sorted = [...genNodes].sort((a, b) => a.x - b.x);
921
+ const romanNum = toRoman(genIdx + 1);
922
+ for (let i = 0; i < sorted.length; i++) {
923
+ genNumbering.set(sorted[i].id, `${romanNum}-${i + 1}`);
924
+ }
925
+ }
926
+ for (const node of nodes) {
927
+ const ind = node.individual;
928
+ const cx = node.x + node.width / 2;
929
+ const labelY = node.y + node.height + 6 + config.fontSize;
930
+ const pedigreeId = genNumbering.get(ind.id) ?? ind.id;
931
+ const displayLabel = ind.label !== ind.id ? ind.label : pedigreeId;
932
+ labels.push(
933
+ text(
934
+ { x: cx, y: labelY, class: "schematex-pedigree-label", "data-individual-id": ind.id },
935
+ displayLabel
936
+ )
937
+ );
938
+ }
939
+ return group({ class: "schematex-pedigree-labels" }, labels);
940
+ }
941
+ function renderGenerationLabels(genGroups, _config) {
942
+ const labels = [];
943
+ const sortedGens = Array.from(genGroups.keys()).sort((a, b) => a - b);
944
+ for (const genIdx of sortedGens) {
945
+ const nodes = genGroups.get(genIdx) ?? [];
946
+ if (nodes.length === 0) continue;
947
+ const midY = nodes[0].y + nodes[0].height / 2;
948
+ const roman = toRoman(genIdx + 1);
949
+ labels.push(
950
+ text(
951
+ { x: 25, y: midY + 5, class: "schematex-pedigree-gen-label" },
952
+ roman
953
+ )
954
+ );
955
+ }
956
+ return group({ class: "schematex-pedigree-generation-labels" }, labels);
957
+ }
958
+ function renderLegend(legendEntries, layout, _config, t) {
959
+ const boxW = 160;
960
+ const rowH = 22;
961
+ const boxH = 30 + legendEntries.length * rowH;
962
+ const x = layout.width - boxW - 20;
963
+ const y = layout.height - boxH - 20;
964
+ const children = [
965
+ rect({ x, y, width: boxW, height: boxH, rx: 4, ry: 4, class: "schematex-pedigree-legend-box" }),
966
+ text({ x: x + boxW / 2, y: y + 18, class: "schematex-pedigree-legend", "text-anchor": "middle", "font-weight": "bold" }, "Legend")
967
+ ];
968
+ for (let i = 0; i < legendEntries.length; i++) {
969
+ const entry = legendEntries[i];
970
+ const ry = y + 30 + i * rowH;
971
+ const swatchSize = 14;
972
+ children.push(
973
+ rect({
974
+ x: x + 10,
975
+ y: ry,
976
+ width: swatchSize,
977
+ height: swatchSize,
978
+ fill: t.conditionFill,
979
+ stroke: t.stroke,
980
+ "stroke-width": "1"
981
+ }),
982
+ text({ x: x + 30, y: ry + 11, class: "schematex-pedigree-legend" }, entry.label)
983
+ );
984
+ }
985
+ return group({ class: "schematex-pedigree-legend-group" }, children);
986
+ }
987
+ function toRoman(n) {
988
+ const vals = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
989
+ const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
990
+ let result = "";
991
+ for (let i = 0; i < vals.length; i++) {
992
+ while (n >= vals[i]) {
993
+ result += syms[i];
994
+ n -= vals[i];
995
+ }
996
+ }
997
+ return result;
998
+ }
999
+
1000
+ // src/diagrams/pedigree/index.ts
1001
+ var pedigree = {
1002
+ type: "pedigree",
1003
+ detect(text2) {
1004
+ const firstLine = text2.trim().split("\n")[0]?.trim().toLowerCase() ?? "";
1005
+ return firstLine === "pedigree" || firstLine.startsWith("pedigree ");
1006
+ },
1007
+ render(text2, config) {
1008
+ const ast = parsePedigree(text2);
1009
+ const layoutConfig = {
1010
+ nodeSpacingX: 80,
1011
+ nodeSpacingY: 100,
1012
+ nodeWidth: 40,
1013
+ nodeHeight: 40
1014
+ };
1015
+ const layout = layoutPedigree(ast, layoutConfig);
1016
+ const renderConfig = {
1017
+ fontFamily: config?.fontFamily ?? "system-ui, -apple-system, sans-serif",
1018
+ fontSize: config?.fontSize ?? 12,
1019
+ theme: config?.theme ?? "default",
1020
+ padding: config?.padding ?? 20
1021
+ };
1022
+ return renderPedigree(layout, renderConfig, ast);
1023
+ }
1024
+ };
1025
+
1026
+ export { PedigreeParseError, layoutPedigree, parsePedigree, pedigree, renderPedigree };
1027
+ //# sourceMappingURL=chunk-LMFSHK45.js.map
1028
+ //# sourceMappingURL=chunk-LMFSHK45.js.map