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.
- package/README.md +126 -7
- package/dist/chunk-F47C6ZEB.js +1041 -0
- package/dist/chunk-F47C6ZEB.js.map +1 -0
- package/dist/chunk-JP4N42AY.js +497 -0
- package/dist/chunk-JP4N42AY.js.map +1 -0
- package/dist/{chunk-P2S7AUOL.js → chunk-LRHHUJFZ.js} +3 -3
- package/dist/{chunk-P2S7AUOL.js.map → chunk-LRHHUJFZ.js.map} +1 -1
- package/dist/{chunk-5B453C4P.js → chunk-O3BT2O42.js} +32 -3
- package/dist/chunk-O3BT2O42.js.map +1 -0
- package/dist/{chunk-EHQMKVDM.js → chunk-Q6DVTCXD.js} +9 -24
- package/dist/chunk-Q6DVTCXD.js.map +1 -0
- package/dist/{chunk-5PGOL2KR.js → chunk-RWPGGWO5.js} +9 -28
- package/dist/chunk-RWPGGWO5.js.map +1 -0
- package/dist/chunk-UJVU7B44.js +764 -0
- package/dist/chunk-UJVU7B44.js.map +1 -0
- package/dist/{chunk-TP3JOOJW.js → chunk-ZBDABVIO.js} +3 -3
- package/dist/{chunk-TP3JOOJW.js.map → chunk-ZBDABVIO.js.map} +1 -1
- package/dist/core/index.cjs +30 -0
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +5 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +32 -21
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.js +2 -2
- package/dist/fishbone/index.js +2 -2
- package/dist/genogram/index.cjs +36 -25
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +4 -2
- package/dist/genogram/index.d.ts +4 -2
- package/dist/genogram/index.js +2 -2
- package/dist/index.cjs +2397 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -5
- package/dist/kinship-DqEklrDN.d.ts +51 -0
- package/dist/kinship-Dy_ijjJV.d.cts +51 -0
- package/dist/labels-CBQ_3Ec9.d.cts +123 -0
- package/dist/labels-DNqRkWuI.d.ts +123 -0
- package/dist/labels-RtFw9tX1.d.cts +91 -0
- package/dist/labels-RtFw9tX1.d.ts +91 -0
- package/dist/labels-iZjijjtK.d.cts +64 -0
- package/dist/labels-iZjijjtK.d.ts +64 -0
- package/dist/locales/pt-br.cjs +77 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +12 -2
- package/dist/locales/pt-br.d.ts +12 -2
- package/dist/locales/pt-br.js +72 -1
- package/dist/locales/pt-br.js.map +1 -1
- package/dist/org-chart/index.cjs +853 -0
- package/dist/org-chart/index.cjs.map +1 -0
- package/dist/org-chart/index.d.cts +168 -0
- package/dist/org-chart/index.d.ts +168 -0
- package/dist/org-chart/index.js +4 -0
- package/dist/org-chart/index.js.map +1 -0
- package/dist/pedigree/index.cjs +1151 -0
- package/dist/pedigree/index.cjs.map +1 -0
- package/dist/pedigree/index.d.cts +155 -0
- package/dist/pedigree/index.d.ts +155 -0
- package/dist/pedigree/index.js +4 -0
- package/dist/pedigree/index.js.map +1 -0
- package/dist/phylo/index.cjs +553 -0
- package/dist/phylo/index.cjs.map +1 -0
- package/dist/phylo/index.d.cts +158 -0
- package/dist/phylo/index.d.ts +158 -0
- package/dist/phylo/index.js +4 -0
- package/dist/phylo/index.js.map +1 -0
- package/dist/types-BnMG7TCd.d.cts +66 -0
- package/dist/types-BnMG7TCd.d.ts +66 -0
- package/package.json +42 -3
- package/dist/chunk-5B453C4P.js.map +0 -1
- package/dist/chunk-5PGOL2KR.js.map +0 -1
- package/dist/chunk-EHQMKVDM.js.map +0 -1
- package/dist/kinship-BARO5-qz.d.cts +0 -115
- package/dist/kinship-Bkf87Jhu.d.ts +0 -115
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,35 @@ function xmlEscape(text) {
|
|
|
5
5
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// src/core/roman.ts
|
|
9
|
+
var ROMAN_TABLE = [
|
|
10
|
+
[1e3, "M"],
|
|
11
|
+
[900, "CM"],
|
|
12
|
+
[500, "D"],
|
|
13
|
+
[400, "CD"],
|
|
14
|
+
[100, "C"],
|
|
15
|
+
[90, "XC"],
|
|
16
|
+
[50, "L"],
|
|
17
|
+
[40, "XL"],
|
|
18
|
+
[10, "X"],
|
|
19
|
+
[9, "IX"],
|
|
20
|
+
[5, "V"],
|
|
21
|
+
[4, "IV"],
|
|
22
|
+
[1, "I"]
|
|
23
|
+
];
|
|
24
|
+
function romanNumeral(n) {
|
|
25
|
+
if (!Number.isInteger(n) || n < 1 || n > 3999) return String(n);
|
|
26
|
+
let remaining = n;
|
|
27
|
+
let out = "";
|
|
28
|
+
for (const [value, symbol] of ROMAN_TABLE) {
|
|
29
|
+
while (remaining >= value) {
|
|
30
|
+
out += symbol;
|
|
31
|
+
remaining -= value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
// src/core/geometry.ts
|
|
9
38
|
function pathData(points) {
|
|
10
39
|
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
@@ -236,14 +265,6 @@ function shapeForSex(sex) {
|
|
|
236
265
|
if (sex === "female") return "circle";
|
|
237
266
|
return "diamond";
|
|
238
267
|
}
|
|
239
|
-
function wrapNodeLabel(displayLabel) {
|
|
240
|
-
const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));
|
|
241
|
-
return wrapLabel(displayLabel, perLine);
|
|
242
|
-
}
|
|
243
|
-
function clampLabel2(label, maxChars) {
|
|
244
|
-
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
245
|
-
return label.slice(0, Math.max(1, maxChars - 1)) + "\u2026";
|
|
246
|
-
}
|
|
247
268
|
var STUB_OFFSETS = [9, -9, 18, -18, 26, -26];
|
|
248
269
|
var ARRIVAL_HALF = NODE_SIZE / 2 - 8;
|
|
249
270
|
function stubOffset(slot) {
|
|
@@ -467,7 +488,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
|
|
|
467
488
|
}
|
|
468
489
|
const measured = /* @__PURE__ */ new Map();
|
|
469
490
|
for (const p of people) {
|
|
470
|
-
const lines =
|
|
491
|
+
const lines = wrapLabelBalanced(clampLabel(p.label, opts.maxLabelChars));
|
|
471
492
|
const contentW = Math.max(NODE_SIZE, lines.reduce((m, l) => Math.max(m, estimateTextWidth(l, LABEL_FONT)), 0));
|
|
472
493
|
measured.set(p.id, { person: p, lines, contentW });
|
|
473
494
|
}
|
|
@@ -1031,11 +1052,6 @@ var STRUCT_WIDTH = 1.5;
|
|
|
1031
1052
|
var STRUCT_OPACITY = 0.75;
|
|
1032
1053
|
var DOTTED_DASH = [2, 3];
|
|
1033
1054
|
var DOTTED_OPACITY = 0.55;
|
|
1034
|
-
var LEGEND_ROW_H2 = 18;
|
|
1035
|
-
var LEGEND_PAD2 = 16;
|
|
1036
|
-
var LEGEND_SWATCH_W2 = 22;
|
|
1037
|
-
var LEGEND_GAP2 = 14;
|
|
1038
|
-
var LEGEND_FONT2 = 11;
|
|
1039
1055
|
function glyphSvg(shape, cx, cy, half) {
|
|
1040
1056
|
const stroke = `fill="transparent" stroke="${GLYPH_STROKE}" stroke-width="2"`;
|
|
1041
1057
|
if (shape === "square") {
|
|
@@ -1145,13 +1161,13 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1145
1161
|
for (const shape of ["square", "circle", "diamond"]) {
|
|
1146
1162
|
if (!shapesUsed.has(shape)) continue;
|
|
1147
1163
|
entries.push({
|
|
1148
|
-
swatch: (x, y) => glyphSvg(shape, x +
|
|
1164
|
+
swatch: (x, y) => glyphSvg(shape, x + LEGEND_SWATCH_W / 2, y, 6),
|
|
1149
1165
|
label: labels.shapes[shape]
|
|
1150
1166
|
});
|
|
1151
1167
|
}
|
|
1152
1168
|
if (layout.nodes.some((n) => n.deceased)) {
|
|
1153
1169
|
entries.push({
|
|
1154
|
-
swatch: (x, y) => glyphSvg("square", x +
|
|
1170
|
+
swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W / 2, y, 6) + `<line x1="${x + LEGEND_SWATCH_W / 2 - 6}" y1="${y - 6}" x2="${x + LEGEND_SWATCH_W / 2 + 6}" y2="${y + 6}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`,
|
|
1155
1171
|
label: labels.deceased
|
|
1156
1172
|
});
|
|
1157
1173
|
}
|
|
@@ -1161,27 +1177,21 @@ function genogramLayoutSvg(layout, opts = {}) {
|
|
|
1161
1177
|
const ink = EDGE_STROKE[style];
|
|
1162
1178
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1163
1179
|
entries.push({
|
|
1164
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1180
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
|
|
1165
1181
|
label: labels.bondStyles[style]
|
|
1166
1182
|
});
|
|
1167
1183
|
}
|
|
1168
1184
|
if (layout.isolatedPersonIds.length > 0) {
|
|
1169
1185
|
entries.push({
|
|
1170
|
-
swatch: (x, y) => glyphSvg("square", x +
|
|
1186
|
+
swatch: (x, y) => glyphSvg("square", x + LEGEND_SWATCH_W / 2, y, 6),
|
|
1171
1187
|
label: labels.isolated
|
|
1172
1188
|
});
|
|
1173
1189
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
return entry.swatch(LEGEND_PAD2, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT2 * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT2}" fill="${GLYPH_STROKE}">${xmlEscape(entry.label)}</text>`;
|
|
1180
|
-
});
|
|
1181
|
-
parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
|
|
1182
|
-
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT2)), 0);
|
|
1183
|
-
width = Math.max(width, LEGEND_PAD2 + LEGEND_SWATCH_W2 + LEGEND_GAP2 + widestLabel + LEGEND_PAD2);
|
|
1184
|
-
height = startY + entries.length * LEGEND_ROW_H2 + LEGEND_PAD2 / 2;
|
|
1190
|
+
const block = legendBlock(entries, layout.height);
|
|
1191
|
+
if (block.svg !== "") {
|
|
1192
|
+
parts.push(block.svg);
|
|
1193
|
+
width = Math.max(width, block.width);
|
|
1194
|
+
height = block.height;
|
|
1185
1195
|
}
|
|
1186
1196
|
}
|
|
1187
1197
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
|
|
@@ -1258,16 +1268,7 @@ var SINGLE_RING_MAX = 8;
|
|
|
1258
1268
|
var NODE_STROKE = "#52525b";
|
|
1259
1269
|
var LABEL_FILL = "#3f3f46";
|
|
1260
1270
|
var EDGE_INK2 = "#71717a";
|
|
1261
|
-
var LEGEND_ROW_H3 = 18;
|
|
1262
|
-
var LEGEND_PAD3 = 16;
|
|
1263
|
-
var LEGEND_SWATCH_W3 = 22;
|
|
1264
|
-
var LEGEND_GAP3 = 14;
|
|
1265
|
-
var LEGEND_FONT3 = 11;
|
|
1266
1271
|
var round = (n) => Math.round(n * 100) / 100;
|
|
1267
|
-
function wrapTieLabel(displayLabel) {
|
|
1268
|
-
const perLine = Math.min(26, Math.max(14, Math.ceil(displayLabel.length / 2) + 2));
|
|
1269
|
-
return wrapLabel(displayLabel, perLine);
|
|
1270
|
-
}
|
|
1271
1272
|
function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
1272
1273
|
const LEN = 9;
|
|
1273
1274
|
const HALF_W = 4.5;
|
|
@@ -1286,7 +1287,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1286
1287
|
const fontSize = opts.fontSize ?? 12;
|
|
1287
1288
|
const labels = opts.labels ?? ECOMAP_LABELS_EN;
|
|
1288
1289
|
const sats = [...input.ties].sort((a, b) => a.id - b.id).map((tie) => {
|
|
1289
|
-
const lines =
|
|
1290
|
+
const lines = wrapLabelBalanced(clampLabel(tie.label, opts.maxLabelChars));
|
|
1290
1291
|
const w2 = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize))) + NODE_PAD_X * 2;
|
|
1291
1292
|
const h2 = lines.length * LINE_H + NODE_PAD_Y * 2;
|
|
1292
1293
|
return {
|
|
@@ -1300,7 +1301,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1300
1301
|
style: qualityLineStyle(tie.quality, opts.qualityLexicon)
|
|
1301
1302
|
};
|
|
1302
1303
|
});
|
|
1303
|
-
const centerLines =
|
|
1304
|
+
const centerLines = wrapLabelBalanced(input.centerLabel);
|
|
1304
1305
|
const centerR = Math.max(
|
|
1305
1306
|
CENTER_MIN_R,
|
|
1306
1307
|
Math.max(...centerLines.map((l) => estimateTextWidth(l, fontSize))) / 2 + 12,
|
|
@@ -1398,7 +1399,7 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1398
1399
|
const entries = [];
|
|
1399
1400
|
if (sats.some((s) => s.style === "plain")) {
|
|
1400
1401
|
entries.push({
|
|
1401
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1402
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="${EDGE_STROKE.plain.width}" stroke-opacity="${EDGE_STROKE.plain.opacity}"/>`,
|
|
1402
1403
|
label: labels.neutralTie
|
|
1403
1404
|
});
|
|
1404
1405
|
}
|
|
@@ -1408,27 +1409,21 @@ function ecomapSvg(input, opts = {}) {
|
|
|
1408
1409
|
const ink = EDGE_STROKE[style];
|
|
1409
1410
|
const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
|
|
1410
1411
|
entries.push({
|
|
1411
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1412
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`,
|
|
1412
1413
|
label: labels.bondStyles[style]
|
|
1413
1414
|
});
|
|
1414
1415
|
}
|
|
1415
1416
|
if (sats.some((s) => s.tie.direction !== null)) {
|
|
1416
1417
|
entries.push({
|
|
1417
|
-
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x +
|
|
1418
|
+
swatch: (x, y) => `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W - 8}" y2="${y}" stroke="${EDGE_INK2}" stroke-width="1.5" stroke-opacity="0.75"/>` + arrowHead(x + LEGEND_SWATCH_W, y, 1, 0, 0.75),
|
|
1418
1419
|
label: labels.direction
|
|
1419
1420
|
});
|
|
1420
1421
|
}
|
|
1421
1422
|
if (entries.length > 0) {
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
return entry.swatch(LEGEND_PAD3, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT3 * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT3}" fill="${NODE_STROKE}">${xmlEscape(entry.label)}</text>`;
|
|
1427
|
-
});
|
|
1428
|
-
parts.push(`<g data-compasso-legend="true">${rows.join("")}</g>`);
|
|
1429
|
-
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT3)), 0);
|
|
1430
|
-
width = Math.max(width, LEGEND_PAD3 + LEGEND_SWATCH_W3 + LEGEND_GAP3 + widestLabel + LEGEND_PAD3);
|
|
1431
|
-
height = startY + entries.length * LEGEND_ROW_H3 + LEGEND_PAD3 / 2;
|
|
1423
|
+
const block = legendBlock(entries, height);
|
|
1424
|
+
parts.push(block.svg);
|
|
1425
|
+
width = Math.max(width, block.width);
|
|
1426
|
+
height = block.height;
|
|
1432
1427
|
}
|
|
1433
1428
|
}
|
|
1434
1429
|
const w = Math.ceil(width);
|
|
@@ -1597,7 +1592,7 @@ function faultTreeIssues(input) {
|
|
|
1597
1592
|
);
|
|
1598
1593
|
}
|
|
1599
1594
|
}
|
|
1600
|
-
const
|
|
1595
|
+
const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
|
|
1601
1596
|
"duplicate-id",
|
|
1602
1597
|
"unknown-input",
|
|
1603
1598
|
"gate-on-non-intermediate",
|
|
@@ -1606,7 +1601,7 @@ function faultTreeIssues(input) {
|
|
|
1606
1601
|
"inhibit-condition",
|
|
1607
1602
|
"top-not-intermediate"
|
|
1608
1603
|
]);
|
|
1609
|
-
if (!issues.some((i) =>
|
|
1604
|
+
if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
|
|
1610
1605
|
const gateOf = (eventId) => gates.find((g) => g.eventId === eventId);
|
|
1611
1606
|
const reachable = /* @__PURE__ */ new Set();
|
|
1612
1607
|
const queue = [input.topId];
|
|
@@ -2381,6 +2376,2293 @@ function fishboneSvg(input, opts = {}) {
|
|
|
2381
2376
|
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>`;
|
|
2382
2377
|
}
|
|
2383
2378
|
|
|
2379
|
+
// src/pedigree/types.ts
|
|
2380
|
+
var LIFE_STATUSES = ["alive", "stillbirth"];
|
|
2381
|
+
|
|
2382
|
+
// src/pedigree/labels.ts
|
|
2383
|
+
var PEDIGREE_TITLE_LABELS_EN = {
|
|
2384
|
+
affected: "affected",
|
|
2385
|
+
carrier: "carrier",
|
|
2386
|
+
deceased: "deceased",
|
|
2387
|
+
proband: "proband",
|
|
2388
|
+
consultand: "consultand",
|
|
2389
|
+
consanguineous: "consanguineous mating",
|
|
2390
|
+
mating: "mating",
|
|
2391
|
+
sibship: "sibship",
|
|
2392
|
+
twins: {
|
|
2393
|
+
mz: "monozygotic twins",
|
|
2394
|
+
dz: "dizygotic twins",
|
|
2395
|
+
unknown: "twins, zygosity unknown"
|
|
2396
|
+
},
|
|
2397
|
+
stillbirth: "stillbirth",
|
|
2398
|
+
generation: "generation"
|
|
2399
|
+
};
|
|
2400
|
+
var PEDIGREE_SVG_LABELS_EN = {
|
|
2401
|
+
shapes: {
|
|
2402
|
+
square: "Male",
|
|
2403
|
+
circle: "Female",
|
|
2404
|
+
diamond: "Sex unknown"
|
|
2405
|
+
},
|
|
2406
|
+
unaffected: "Unaffected",
|
|
2407
|
+
carrier: "Carrier",
|
|
2408
|
+
deceased: "Deceased",
|
|
2409
|
+
proband: "Proband",
|
|
2410
|
+
consultand: "Consultand",
|
|
2411
|
+
consanguineous: "Consanguineous mating",
|
|
2412
|
+
twins: {
|
|
2413
|
+
mz: "Monozygotic twins",
|
|
2414
|
+
dz: "Dizygotic twins",
|
|
2415
|
+
unknown: "Twins (zygosity unknown)"
|
|
2416
|
+
},
|
|
2417
|
+
stillbirth: "Stillbirth",
|
|
2418
|
+
isolated: "No recorded relatives",
|
|
2419
|
+
ariaLabel: "Pedigree chart"
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
// src/pedigree/validate.ts
|
|
2423
|
+
var PedigreeValidationError = class extends Error {
|
|
2424
|
+
issues;
|
|
2425
|
+
constructor(issues) {
|
|
2426
|
+
super(`invalid pedigree: ${issues.map((i) => i.message).join("; ")}`);
|
|
2427
|
+
this.name = "PedigreeValidationError";
|
|
2428
|
+
this.issues = issues;
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
var MAX_CONDITIONS_PER_INDIVIDUAL = 4;
|
|
2432
|
+
function sortIssues2(issues) {
|
|
2433
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2434
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
2435
|
+
return [...unique.values()].sort(
|
|
2436
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
function pedigreeIssues(input) {
|
|
2440
|
+
if (input.individuals.length === 0 && input.matings.length === 0 && input.sibships.length === 0) return [];
|
|
2441
|
+
const issues = [];
|
|
2442
|
+
const push = (code, message) => {
|
|
2443
|
+
issues.push({ code, message });
|
|
2444
|
+
};
|
|
2445
|
+
const individualById = /* @__PURE__ */ new Map();
|
|
2446
|
+
const dupIndividualIds = /* @__PURE__ */ new Set();
|
|
2447
|
+
for (const ind of input.individuals) {
|
|
2448
|
+
if (individualById.has(ind.id)) dupIndividualIds.add(ind.id);
|
|
2449
|
+
else individualById.set(ind.id, ind);
|
|
2450
|
+
}
|
|
2451
|
+
for (const id of [...dupIndividualIds].sort((a, b) => a - b)) {
|
|
2452
|
+
push("duplicate-id", `duplicate individual id ${id}`);
|
|
2453
|
+
}
|
|
2454
|
+
const matingById = /* @__PURE__ */ new Map();
|
|
2455
|
+
const dupMatingIds = /* @__PURE__ */ new Set();
|
|
2456
|
+
for (const m of input.matings) {
|
|
2457
|
+
if (matingById.has(m.id)) dupMatingIds.add(m.id);
|
|
2458
|
+
else matingById.set(m.id, m);
|
|
2459
|
+
}
|
|
2460
|
+
for (const id of [...dupMatingIds].sort((a, b) => a - b)) {
|
|
2461
|
+
push("duplicate-id", `duplicate mating id ${id}`);
|
|
2462
|
+
}
|
|
2463
|
+
const sibshipById = /* @__PURE__ */ new Map();
|
|
2464
|
+
const dupSibshipIds = /* @__PURE__ */ new Set();
|
|
2465
|
+
for (const s of input.sibships) {
|
|
2466
|
+
if (sibshipById.has(s.id)) dupSibshipIds.add(s.id);
|
|
2467
|
+
else sibshipById.set(s.id, s);
|
|
2468
|
+
}
|
|
2469
|
+
for (const id of [...dupSibshipIds].sort((a, b) => a - b)) {
|
|
2470
|
+
push("duplicate-id", `duplicate sibship id ${id}`);
|
|
2471
|
+
}
|
|
2472
|
+
if (issues.length > 0) {
|
|
2473
|
+
return sortIssues2(issues);
|
|
2474
|
+
}
|
|
2475
|
+
const individuals = [...individualById.values()].sort((a, b) => a.id - b.id);
|
|
2476
|
+
const matings = [...matingById.values()].sort((a, b) => a.id - b.id);
|
|
2477
|
+
const sibships = [...sibshipById.values()].sort((a, b) => a.id - b.id);
|
|
2478
|
+
const conditionIds = new Set(input.conditions.map((c) => c.id));
|
|
2479
|
+
for (const m of matings) {
|
|
2480
|
+
if (!individualById.has(m.partnerAId)) {
|
|
2481
|
+
push("unknown-partner", `mating ${m.id} references unknown individual ${m.partnerAId}`);
|
|
2482
|
+
}
|
|
2483
|
+
if (!individualById.has(m.partnerBId)) {
|
|
2484
|
+
push("unknown-partner", `mating ${m.id} references unknown individual ${m.partnerBId}`);
|
|
2485
|
+
}
|
|
2486
|
+
if (m.partnerAId === m.partnerBId) {
|
|
2487
|
+
push("self-mating", `mating ${m.id} mates individual ${m.partnerAId} with itself`);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
for (const s of sibships) {
|
|
2491
|
+
if (!matingById.has(s.matingId)) {
|
|
2492
|
+
push("unknown-sibship-mating", `sibship ${s.id} references unknown mating ${s.matingId}`);
|
|
2493
|
+
}
|
|
2494
|
+
for (const childId of s.childIds) {
|
|
2495
|
+
if (!individualById.has(childId)) {
|
|
2496
|
+
push("unknown-child", `sibship ${s.id} references unknown child ${childId}`);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
const childSet = new Set(s.childIds);
|
|
2500
|
+
const offending = /* @__PURE__ */ new Set();
|
|
2501
|
+
for (const tg of s.twinGroups) {
|
|
2502
|
+
for (const memberId of tg.childIds) {
|
|
2503
|
+
if (!childSet.has(memberId)) offending.add(memberId);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
for (const memberId of [...offending].sort((a, b) => a - b)) {
|
|
2507
|
+
push("twin-not-in-sibship", `twin ${memberId} is not a child of sibship ${s.id}`);
|
|
2508
|
+
}
|
|
2509
|
+
for (const tg of s.twinGroups) {
|
|
2510
|
+
if (tg.childIds.length < 2) {
|
|
2511
|
+
const [only] = tg.childIds;
|
|
2512
|
+
push(
|
|
2513
|
+
"twin-group-too-small",
|
|
2514
|
+
`twin group in sibship ${s.id} has only 1 member (${only ?? "none"}) \u2014 at least 2 required`
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
for (const tg of s.twinGroups) {
|
|
2519
|
+
if (tg.childIds.length < 2) continue;
|
|
2520
|
+
const memberSet = new Set(tg.childIds);
|
|
2521
|
+
const positions = s.childIds.map((cid, pos) => memberSet.has(cid) ? pos : -1).filter((p) => p >= 0);
|
|
2522
|
+
if (positions.length === 0) continue;
|
|
2523
|
+
const first = positions[0];
|
|
2524
|
+
const last = positions[positions.length - 1];
|
|
2525
|
+
if (last - first !== positions.length - 1) {
|
|
2526
|
+
const memberList = [...tg.childIds].sort((a, b) => a - b).join(", ");
|
|
2527
|
+
push(
|
|
2528
|
+
"twin-group-not-contiguous",
|
|
2529
|
+
`twin group [${memberList}] in sibship ${s.id} is not a contiguous run in childIds`
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const sibshipsOfChild = /* @__PURE__ */ new Map();
|
|
2535
|
+
for (const s of sibships) {
|
|
2536
|
+
for (const childId of s.childIds) {
|
|
2537
|
+
const arr = sibshipsOfChild.get(childId) ?? [];
|
|
2538
|
+
arr.push(s.id);
|
|
2539
|
+
sibshipsOfChild.set(childId, arr);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
for (const [childId, sibIds] of [...sibshipsOfChild.entries()].sort((a, b) => a[0] - b[0])) {
|
|
2543
|
+
if (sibIds.length > 1) {
|
|
2544
|
+
push(
|
|
2545
|
+
"child-of-two-sibships",
|
|
2546
|
+
`individual ${childId} is a child of sibships ${[...sibIds].sort((a, b) => a - b).join(", ")}`
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const ind of individuals) {
|
|
2551
|
+
if (!Number.isInteger(ind.generation)) {
|
|
2552
|
+
push("generation-not-integer", `individual ${ind.id} has non-integer generation ${ind.generation}`);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
for (const ind of individuals) {
|
|
2556
|
+
for (const conditionId of ind.affectedBy) {
|
|
2557
|
+
if (!conditionIds.has(conditionId)) {
|
|
2558
|
+
push("unknown-condition", `individual ${ind.id} is affected by unknown condition ${conditionId}`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (ind.affectedBy.length > MAX_CONDITIONS_PER_INDIVIDUAL) {
|
|
2562
|
+
push(
|
|
2563
|
+
"too-many-conditions",
|
|
2564
|
+
`individual ${ind.id} has ${ind.affectedBy.length} conditions \u2014 at most ${MAX_CONDITIONS_PER_INDIVIDUAL} can be drawn`
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
|
|
2569
|
+
"duplicate-id",
|
|
2570
|
+
"unknown-partner",
|
|
2571
|
+
"unknown-sibship-mating",
|
|
2572
|
+
"unknown-child"
|
|
2573
|
+
]);
|
|
2574
|
+
if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
|
|
2575
|
+
for (const s of sibships) {
|
|
2576
|
+
const mating = matingById.get(s.matingId);
|
|
2577
|
+
if (mating === void 0) continue;
|
|
2578
|
+
const a = individualById.get(mating.partnerAId);
|
|
2579
|
+
const b = individualById.get(mating.partnerBId);
|
|
2580
|
+
const parentGenerations = [a, b].filter((p) => p !== void 0 && Number.isInteger(p.generation)).map((p) => p.generation);
|
|
2581
|
+
if (parentGenerations.length === 0) continue;
|
|
2582
|
+
const parentRow = Math.max(...parentGenerations);
|
|
2583
|
+
for (const childId of s.childIds) {
|
|
2584
|
+
const child = individualById.get(childId);
|
|
2585
|
+
if (child === void 0 || !Number.isInteger(child.generation)) continue;
|
|
2586
|
+
if (child.generation <= parentRow) {
|
|
2587
|
+
push(
|
|
2588
|
+
"generation-inversion",
|
|
2589
|
+
`child ${childId} (generation ${child.generation}) is not below its parents (generation ${parentRow}) in sibship ${s.id}`
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return sortIssues2(issues);
|
|
2596
|
+
}
|
|
2597
|
+
function validatePedigree(input) {
|
|
2598
|
+
const issues = pedigreeIssues(input);
|
|
2599
|
+
if (issues.length > 0) throw new PedigreeValidationError(issues);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/pedigree/layout.ts
|
|
2603
|
+
var PED_GLYPH = 40;
|
|
2604
|
+
var PED_LABEL_FONT = 12;
|
|
2605
|
+
var PED_LABEL_LINE_H = 14;
|
|
2606
|
+
var PED_ADDRESS_FONT = 10;
|
|
2607
|
+
var PED_LABEL_GAP = 6;
|
|
2608
|
+
var PADDING5 = 32;
|
|
2609
|
+
var GUTTER = 36;
|
|
2610
|
+
var H_GAP2 = 28;
|
|
2611
|
+
var MATING_RUN = 22;
|
|
2612
|
+
var CORRIDOR2 = 44;
|
|
2613
|
+
var TWIN_JUNCTION_DROP = 12;
|
|
2614
|
+
var BRIDGE_DIP_GAP = 10;
|
|
2615
|
+
var BRIDGE_DIP_STEP = 7;
|
|
2616
|
+
var BRIDGE_STUB_X_STEP = 3;
|
|
2617
|
+
var PED_MATING_ID_BASE = 1e6;
|
|
2618
|
+
var PED_DESCENT_ID_BASE = 2e6;
|
|
2619
|
+
var PED_SIBBAR_ID_BASE = 3e6;
|
|
2620
|
+
var PED_RISER_ID_BASE = 4e6;
|
|
2621
|
+
var PED_TWINBAR_ID_BASE = 5e6;
|
|
2622
|
+
var round5 = (n) => Math.round(n * 100) / 100;
|
|
2623
|
+
function shapeForSex2(sex) {
|
|
2624
|
+
if (sex === "male") return "square";
|
|
2625
|
+
if (sex === "female") return "circle";
|
|
2626
|
+
return "diamond";
|
|
2627
|
+
}
|
|
2628
|
+
var PED_CONDITION_FILLS = ["#52525b", "#a1a1aa", "#3f3f46", "#71717a"];
|
|
2629
|
+
function wrapPedigreeLabel(displayLabel) {
|
|
2630
|
+
if (displayLabel === "") return [];
|
|
2631
|
+
return wrapLabelBalanced(displayLabel, 2);
|
|
2632
|
+
}
|
|
2633
|
+
function individualTitle(ind, address, conditionLabelById, titleLabels) {
|
|
2634
|
+
if (ind.title !== void 0) return ind.title;
|
|
2635
|
+
const parts = [`${address} \xB7 ${ind.label}`];
|
|
2636
|
+
if (ind.affectedBy.length > 0) {
|
|
2637
|
+
const names = ind.affectedBy.map((id) => conditionLabelById.get(id) ?? String(id));
|
|
2638
|
+
parts.push(`${titleLabels.affected}: ${names.join(", ")}`);
|
|
2639
|
+
}
|
|
2640
|
+
if (ind.carrier && ind.affectedBy.length === 0) parts.push(titleLabels.carrier);
|
|
2641
|
+
if (ind.deceased) parts.push(titleLabels.deceased);
|
|
2642
|
+
if (ind.role === "proband") parts.push(titleLabels.proband);
|
|
2643
|
+
else if (ind.role === "consultand") parts.push(titleLabels.consultand);
|
|
2644
|
+
if (ind.lifeStatus === "stillbirth") parts.push(titleLabels.stillbirth);
|
|
2645
|
+
return parts.join(" \xB7 ");
|
|
2646
|
+
}
|
|
2647
|
+
function computePedigreeLayout(input, opts = {}) {
|
|
2648
|
+
if (input.individuals.length === 0 && input.matings.length === 0 && input.sibships.length === 0) {
|
|
2649
|
+
return {
|
|
2650
|
+
width: PADDING5 * 2,
|
|
2651
|
+
height: PADDING5 * 2,
|
|
2652
|
+
nodes: [],
|
|
2653
|
+
elements: [],
|
|
2654
|
+
generations: [],
|
|
2655
|
+
conditionFills: [],
|
|
2656
|
+
twinZygositiesUsed: [],
|
|
2657
|
+
unknownTwinJunctions: [],
|
|
2658
|
+
isolatedIndividualIds: []
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
validatePedigree(input);
|
|
2662
|
+
const titleLabels = opts.titleLabels ?? PEDIGREE_TITLE_LABELS_EN;
|
|
2663
|
+
const individuals = [...input.individuals].sort((a, b) => a.id - b.id);
|
|
2664
|
+
const individualById = new Map(individuals.map((i) => [i.id, i]));
|
|
2665
|
+
const matings = [...input.matings].sort((a, b) => a.id - b.id);
|
|
2666
|
+
const sibships = [...input.sibships].sort((a, b) => a.id - b.id);
|
|
2667
|
+
const conditionLabelById = new Map(input.conditions.map((c) => [c.id, c.label]));
|
|
2668
|
+
const partnersOf = (m) => m.partnerAId <= m.partnerBId ? [m.partnerAId, m.partnerBId] : [m.partnerBId, m.partnerAId];
|
|
2669
|
+
const distinctGenerations = [...new Set(individuals.map((i) => i.generation))].sort((a, b) => a - b);
|
|
2670
|
+
const rowOfGeneration = /* @__PURE__ */ new Map();
|
|
2671
|
+
distinctGenerations.forEach((g, i) => rowOfGeneration.set(g, i));
|
|
2672
|
+
const rowCount = distinctGenerations.length;
|
|
2673
|
+
const labelLinesOf = /* @__PURE__ */ new Map();
|
|
2674
|
+
for (const ind of individuals) {
|
|
2675
|
+
labelLinesOf.set(ind.id, wrapPedigreeLabel(clampLabel(ind.label, opts.maxLabelChars)));
|
|
2676
|
+
}
|
|
2677
|
+
const nodeHalfWidth = (ind) => {
|
|
2678
|
+
const lines = labelLinesOf.get(ind.id) ?? [];
|
|
2679
|
+
const labelW = lines.reduce((m, l) => Math.max(m, estimateTextWidth(l, PED_LABEL_FONT)), 0);
|
|
2680
|
+
return Math.max(PED_GLYPH, labelW) / 2;
|
|
2681
|
+
};
|
|
2682
|
+
const childToSibship = /* @__PURE__ */ new Map();
|
|
2683
|
+
for (const s of sibships) for (const c of s.childIds) childToSibship.set(c, s);
|
|
2684
|
+
const sibshipByMating = /* @__PURE__ */ new Map();
|
|
2685
|
+
for (const s of sibships) {
|
|
2686
|
+
const arr = sibshipByMating.get(s.matingId) ?? [];
|
|
2687
|
+
arr.push(s);
|
|
2688
|
+
sibshipByMating.set(s.matingId, arr);
|
|
2689
|
+
}
|
|
2690
|
+
const matingsOfIndividual = /* @__PURE__ */ new Map();
|
|
2691
|
+
for (const m of matings) {
|
|
2692
|
+
for (const p of partnersOf(m)) {
|
|
2693
|
+
const arr = matingsOfIndividual.get(p) ?? [];
|
|
2694
|
+
arr.push(m);
|
|
2695
|
+
matingsOfIndividual.set(p, arr);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
const placedById = /* @__PURE__ */ new Map();
|
|
2699
|
+
let cursorX = PADDING5 + GUTTER;
|
|
2700
|
+
const placeLeaf = (ind, leftEdge) => {
|
|
2701
|
+
const half = nodeHalfWidth(ind);
|
|
2702
|
+
const cx = leftEdge + half;
|
|
2703
|
+
const placed = { ind, cx, spanL: half, spanR: half };
|
|
2704
|
+
placedById.set(ind.id, placed);
|
|
2705
|
+
return placed;
|
|
2706
|
+
};
|
|
2707
|
+
const placedMatings = /* @__PURE__ */ new Set();
|
|
2708
|
+
const matingCount = (id) => (matingsOfIndividual.get(id) ?? []).length;
|
|
2709
|
+
const hubCursor = /* @__PURE__ */ new Map();
|
|
2710
|
+
const hubSpouseSeq = /* @__PURE__ */ new Map();
|
|
2711
|
+
const packMating = (mating, leftEdge) => {
|
|
2712
|
+
placedMatings.add(mating.id);
|
|
2713
|
+
const [aId, bId] = partnersOf(mating);
|
|
2714
|
+
const a = individualById.get(aId);
|
|
2715
|
+
const b = individualById.get(bId);
|
|
2716
|
+
const aHalf = nodeHalfWidth(a);
|
|
2717
|
+
const bHalf = nodeHalfWidth(b);
|
|
2718
|
+
const halfSep = Math.max(aHalf, bHalf) + MATING_RUN;
|
|
2719
|
+
const aPlaced = placedById.get(aId);
|
|
2720
|
+
const bPlaced = placedById.get(bId);
|
|
2721
|
+
const hubId = aPlaced !== void 0 ? aId : bPlaced !== void 0 ? bId : matingCount(aId) >= 2 ? aId : matingCount(bId) >= 2 ? bId : null;
|
|
2722
|
+
const blockLeft = hubId !== null && hubCursor.has(hubId) ? hubCursor.get(hubId) : leftEdge;
|
|
2723
|
+
const sibs = (sibshipByMating.get(mating.id) ?? []).sort((s1, s2) => s1.id - s2.id);
|
|
2724
|
+
const childIds = sibs.flatMap((s) => s.childIds);
|
|
2725
|
+
let childCursor = blockLeft;
|
|
2726
|
+
let childLeft = Number.POSITIVE_INFINITY;
|
|
2727
|
+
let childrenRight = blockLeft;
|
|
2728
|
+
let placedAnyChild = false;
|
|
2729
|
+
for (const childId of childIds) {
|
|
2730
|
+
const child = individualById.get(childId);
|
|
2731
|
+
if (child === void 0 || placedById.has(childId)) continue;
|
|
2732
|
+
const downMating = (matingsOfIndividual.get(childId) ?? []).find((m) => {
|
|
2733
|
+
if (placedMatings.has(m.id)) return false;
|
|
2734
|
+
const [pa, pb] = partnersOf(m);
|
|
2735
|
+
const otherId = pa === childId ? pb : pa;
|
|
2736
|
+
const other = individualById.get(otherId);
|
|
2737
|
+
return other === void 0 || child.generation <= other.generation;
|
|
2738
|
+
});
|
|
2739
|
+
if (downMating !== void 0) {
|
|
2740
|
+
const right = packMating(downMating, childCursor);
|
|
2741
|
+
childLeft = Math.min(childLeft, placedById.get(childId).cx - nodeHalfWidth(child));
|
|
2742
|
+
childrenRight = right;
|
|
2743
|
+
childCursor = right + H_GAP2;
|
|
2744
|
+
} else {
|
|
2745
|
+
const placed = placeLeaf(child, childCursor);
|
|
2746
|
+
childLeft = Math.min(childLeft, placed.cx - placed.spanL);
|
|
2747
|
+
childrenRight = placed.cx + placed.spanR;
|
|
2748
|
+
childCursor = childrenRight + H_GAP2;
|
|
2749
|
+
}
|
|
2750
|
+
placedAnyChild = true;
|
|
2751
|
+
}
|
|
2752
|
+
const bandMid = placedAnyChild ? (childLeft + childrenRight) / 2 : blockLeft + halfSep;
|
|
2753
|
+
if (hubId === null) {
|
|
2754
|
+
const pairMid = placedAnyChild ? bandMid : blockLeft + Math.max(aHalf, bHalf) + halfSep;
|
|
2755
|
+
placedById.set(aId, { ind: a, cx: pairMid - halfSep, spanL: aHalf, spanR: aHalf });
|
|
2756
|
+
placedById.set(bId, { ind: b, cx: pairMid + halfSep, spanL: bHalf, spanR: bHalf });
|
|
2757
|
+
} else {
|
|
2758
|
+
const spouseId = hubId === aId ? bId : aId;
|
|
2759
|
+
const spouse = individualById.get(spouseId);
|
|
2760
|
+
const spouseHalf = nodeHalfWidth(spouse);
|
|
2761
|
+
const hubHalf = nodeHalfWidth(individualById.get(hubId));
|
|
2762
|
+
const seq = hubSpouseSeq.get(hubId) ?? 0;
|
|
2763
|
+
const hubExisting = placedById.get(hubId);
|
|
2764
|
+
const minSpouseCx = blockLeft + hubHalf + halfSep;
|
|
2765
|
+
let spouseCx = placedAnyChild ? Math.max(bandMid, minSpouseCx) : minSpouseCx + spouseHalf;
|
|
2766
|
+
if (hubExisting === void 0) {
|
|
2767
|
+
placedById.set(hubId, { ind: individualById.get(hubId), cx: spouseCx - halfSep, spanL: hubHalf, spanR: hubHalf });
|
|
2768
|
+
} else {
|
|
2769
|
+
spouseCx = Math.max(spouseCx, hubExisting.cx + halfSep);
|
|
2770
|
+
}
|
|
2771
|
+
placedById.set(spouseId, { ind: spouse, cx: spouseCx, spanL: spouseHalf, spanR: spouseHalf });
|
|
2772
|
+
hubSpouseSeq.set(hubId, seq + 1);
|
|
2773
|
+
}
|
|
2774
|
+
const blockRight = Math.max(
|
|
2775
|
+
childrenRight,
|
|
2776
|
+
placedById.get(aId).cx + nodeHalfWidth(a),
|
|
2777
|
+
placedById.get(bId).cx + nodeHalfWidth(b)
|
|
2778
|
+
);
|
|
2779
|
+
if (hubId !== null) hubCursor.set(hubId, blockRight + H_GAP2);
|
|
2780
|
+
return blockRight;
|
|
2781
|
+
};
|
|
2782
|
+
const generationOfMating = (m) => {
|
|
2783
|
+
const [aId, bId] = partnersOf(m);
|
|
2784
|
+
const a = individualById.get(aId);
|
|
2785
|
+
const b = individualById.get(bId);
|
|
2786
|
+
const gens = [a, b].filter((p) => p !== void 0).map((p) => p.generation);
|
|
2787
|
+
return gens.length > 0 ? Math.min(...gens) : 0;
|
|
2788
|
+
};
|
|
2789
|
+
const rootMatings = matings.filter((m) => {
|
|
2790
|
+
const [aId, bId] = partnersOf(m);
|
|
2791
|
+
return !childToSibship.has(aId) && !childToSibship.has(bId);
|
|
2792
|
+
}).sort((m1, m2) => generationOfMating(m1) - generationOfMating(m2) || m1.id - m2.id);
|
|
2793
|
+
for (const m of rootMatings) {
|
|
2794
|
+
if (placedMatings.has(m.id)) continue;
|
|
2795
|
+
const right = packMating(m, cursorX);
|
|
2796
|
+
cursorX = right + H_GAP2 * 2;
|
|
2797
|
+
}
|
|
2798
|
+
for (const m of matings) {
|
|
2799
|
+
if (placedMatings.has(m.id)) continue;
|
|
2800
|
+
const right = packMating(m, cursorX);
|
|
2801
|
+
cursorX = right + H_GAP2 * 2;
|
|
2802
|
+
}
|
|
2803
|
+
const inAnyMating = /* @__PURE__ */ new Set();
|
|
2804
|
+
for (const m of matings) for (const p of partnersOf(m)) inAnyMating.add(p);
|
|
2805
|
+
const isolatedIndividualIds = individuals.filter((i) => !inAnyMating.has(i.id) && !childToSibship.has(i.id)).map((i) => i.id);
|
|
2806
|
+
for (const id of isolatedIndividualIds) {
|
|
2807
|
+
const ind = individualById.get(id);
|
|
2808
|
+
if (placedById.has(id)) continue;
|
|
2809
|
+
placeLeaf(ind, cursorX);
|
|
2810
|
+
cursorX = placedById.get(id).cx + nodeHalfWidth(ind) + H_GAP2;
|
|
2811
|
+
}
|
|
2812
|
+
for (const ind of individuals) {
|
|
2813
|
+
if (placedById.has(ind.id)) continue;
|
|
2814
|
+
placeLeaf(ind, cursorX);
|
|
2815
|
+
cursorX = placedById.get(ind.id).cx + nodeHalfWidth(ind) + H_GAP2;
|
|
2816
|
+
}
|
|
2817
|
+
const matingBridgeSlot = /* @__PURE__ */ new Map();
|
|
2818
|
+
{
|
|
2819
|
+
const dipSlotByRow = /* @__PURE__ */ new Map();
|
|
2820
|
+
for (const m of [...matings].sort((m1, m2) => m1.id - m2.id)) {
|
|
2821
|
+
const [aId, bId] = partnersOf(m);
|
|
2822
|
+
const a = individualById.get(aId);
|
|
2823
|
+
const b = individualById.get(bId);
|
|
2824
|
+
if (a === void 0 || b === void 0) continue;
|
|
2825
|
+
if (a.generation !== b.generation) continue;
|
|
2826
|
+
const row = rowOfGeneration.get(a.generation);
|
|
2827
|
+
const loX = Math.min(placedById.get(aId).cx, placedById.get(bId).cx);
|
|
2828
|
+
const hiX = Math.max(placedById.get(aId).cx, placedById.get(bId).cx);
|
|
2829
|
+
const blocked = individuals.some(
|
|
2830
|
+
(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
|
|
2831
|
+
);
|
|
2832
|
+
if (!blocked) {
|
|
2833
|
+
matingBridgeSlot.set(m.id, 0);
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const slot = (dipSlotByRow.get(row) ?? 0) + 1;
|
|
2837
|
+
dipSlotByRow.set(row, slot);
|
|
2838
|
+
matingBridgeSlot.set(m.id, slot);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
const rowDipBand = new Array(rowCount).fill(0);
|
|
2842
|
+
for (const m of matings) {
|
|
2843
|
+
const slot = matingBridgeSlot.get(m.id) ?? 0;
|
|
2844
|
+
if (slot < 1) continue;
|
|
2845
|
+
const [aId, bId] = partnersOf(m);
|
|
2846
|
+
const a = individualById.get(aId);
|
|
2847
|
+
const b = individualById.get(bId);
|
|
2848
|
+
const gens = [a, b].filter((p) => p !== void 0).map((p) => p.generation);
|
|
2849
|
+
if (gens.length === 0) continue;
|
|
2850
|
+
const row = rowOfGeneration.get(Math.min(...gens));
|
|
2851
|
+
rowDipBand[row] = Math.max(rowDipBand[row], BRIDGE_DIP_GAP + slot * BRIDGE_DIP_STEP + PED_LABEL_GAP);
|
|
2852
|
+
}
|
|
2853
|
+
const rowHeight = new Array(rowCount).fill(0);
|
|
2854
|
+
for (const ind of individuals) {
|
|
2855
|
+
const row = rowOfGeneration.get(ind.generation);
|
|
2856
|
+
const lines = labelLinesOf.get(ind.id) ?? [];
|
|
2857
|
+
const h = PED_GLYPH + PED_LABEL_GAP + rowDipBand[row] + (lines.length + 1) * PED_LABEL_LINE_H;
|
|
2858
|
+
rowHeight[row] = Math.max(rowHeight[row], h);
|
|
2859
|
+
}
|
|
2860
|
+
const rowTop = [PADDING5];
|
|
2861
|
+
for (let r = 0; r < rowCount - 1; r++) {
|
|
2862
|
+
rowTop.push(rowTop[r] + rowHeight[r] + CORRIDOR2);
|
|
2863
|
+
}
|
|
2864
|
+
const glyphCyOfRow = (row) => rowTop[row] + PED_GLYPH / 2;
|
|
2865
|
+
const addressByIndividual = /* @__PURE__ */ new Map();
|
|
2866
|
+
for (let row = 0; row < rowCount; row++) {
|
|
2867
|
+
const roman = romanNumeral(row + 1);
|
|
2868
|
+
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);
|
|
2869
|
+
inRow.forEach((ind, i) => addressByIndividual.set(ind.id, `${roman}-${i + 1}`));
|
|
2870
|
+
}
|
|
2871
|
+
const declaredConditionIds = new Set(input.conditions.map((c) => c.id));
|
|
2872
|
+
const usedConditionIds = [...new Set(individuals.flatMap((i) => i.affectedBy))].filter((id) => declaredConditionIds.has(id)).sort((x, y) => x - y);
|
|
2873
|
+
const conditionFills = usedConditionIds.map((id, i) => ({
|
|
2874
|
+
id,
|
|
2875
|
+
ink: PED_CONDITION_FILLS[i % PED_CONDITION_FILLS.length],
|
|
2876
|
+
label: conditionLabelById.get(id) ?? `condition ${id}`
|
|
2877
|
+
}));
|
|
2878
|
+
const nodes = [];
|
|
2879
|
+
for (const ind of individuals) {
|
|
2880
|
+
const placed = placedById.get(ind.id);
|
|
2881
|
+
const row = rowOfGeneration.get(ind.generation);
|
|
2882
|
+
const cy = glyphCyOfRow(row);
|
|
2883
|
+
const lines = labelLinesOf.get(ind.id) ?? [];
|
|
2884
|
+
const address = addressByIndividual.get(ind.id);
|
|
2885
|
+
nodes.push({
|
|
2886
|
+
individualId: ind.id,
|
|
2887
|
+
shape: shapeForSex2(ind.sex),
|
|
2888
|
+
cx: round5(placed.cx),
|
|
2889
|
+
cy: round5(cy),
|
|
2890
|
+
size: PED_GLYPH,
|
|
2891
|
+
deceased: ind.deceased,
|
|
2892
|
+
carrier: ind.carrier,
|
|
2893
|
+
role: ind.role,
|
|
2894
|
+
stillbirth: ind.lifeStatus === "stillbirth",
|
|
2895
|
+
affectedBy: ind.affectedBy,
|
|
2896
|
+
labelLines: lines,
|
|
2897
|
+
// Labels sit BELOW the row's serial-union dip band (rowDipBand) so a hub's bridge dips
|
|
2898
|
+
// never cross a label box.
|
|
2899
|
+
labelTop: round5(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
|
|
2900
|
+
addressLabel: address,
|
|
2901
|
+
title: individualTitle(ind, address, conditionLabelById, titleLabels)
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
const elements = [];
|
|
2905
|
+
const cxOf = (id) => placedById.get(id).cx;
|
|
2906
|
+
const cyOf = (id) => glyphCyOfRow(rowOfGeneration.get(individualById.get(id).generation));
|
|
2907
|
+
const GLYPH_HALF = PED_GLYPH / 2;
|
|
2908
|
+
const matingBarCenter = /* @__PURE__ */ new Map();
|
|
2909
|
+
for (const s of sibships) {
|
|
2910
|
+
const childIds = s.childIds.filter((id) => placedById.has(id));
|
|
2911
|
+
if (childIds.length === 0) continue;
|
|
2912
|
+
const xs = childIds.map(cxOf);
|
|
2913
|
+
const center = (Math.min(...xs) + Math.max(...xs)) / 2;
|
|
2914
|
+
matingBarCenter.set(s.matingId, center);
|
|
2915
|
+
}
|
|
2916
|
+
const matingMidpoint = /* @__PURE__ */ new Map();
|
|
2917
|
+
const bridgeDipY = (slot) => BRIDGE_DIP_GAP + slot * BRIDGE_DIP_STEP;
|
|
2918
|
+
for (const m of matings) {
|
|
2919
|
+
const [aId, bId] = partnersOf(m);
|
|
2920
|
+
const ax = cxOf(aId);
|
|
2921
|
+
const bx = cxOf(bId);
|
|
2922
|
+
const ay = cyOf(aId);
|
|
2923
|
+
const by = cyOf(bId);
|
|
2924
|
+
const leftId = ax <= bx ? aId : bId;
|
|
2925
|
+
const rightId = ax <= bx ? bId : aId;
|
|
2926
|
+
const lx = cxOf(leftId) + GLYPH_HALF;
|
|
2927
|
+
const rx = cxOf(rightId) - GLYPH_HALF;
|
|
2928
|
+
const title = m.consanguineous ? titleLabels.consanguineous : titleLabels.mating;
|
|
2929
|
+
const bridgeSlot = matingBridgeSlot.get(m.id) ?? 0;
|
|
2930
|
+
if (ay === by && bridgeSlot >= 1) {
|
|
2931
|
+
const hubCx = cxOf(leftId);
|
|
2932
|
+
const spouseCx = cxOf(rightId);
|
|
2933
|
+
const glyphBottom = ay + GLYPH_HALF;
|
|
2934
|
+
const dipY = glyphBottom + bridgeDipY(bridgeSlot);
|
|
2935
|
+
const stubOff = Math.min(bridgeSlot * BRIDGE_STUB_X_STEP, GLYPH_HALF - 4);
|
|
2936
|
+
const hubStubX = hubCx + stubOff;
|
|
2937
|
+
const spouseStubX = spouseCx - stubOff;
|
|
2938
|
+
elements.push({
|
|
2939
|
+
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2940
|
+
kind: "mating-elbow",
|
|
2941
|
+
points: [
|
|
2942
|
+
{ x: round5(hubStubX), y: round5(glyphBottom) },
|
|
2943
|
+
{ x: round5(hubStubX), y: round5(dipY) },
|
|
2944
|
+
{ x: round5(spouseStubX), y: round5(dipY) },
|
|
2945
|
+
{ x: round5(spouseStubX), y: round5(glyphBottom) }
|
|
2946
|
+
],
|
|
2947
|
+
consanguineous: m.consanguineous,
|
|
2948
|
+
title
|
|
2949
|
+
});
|
|
2950
|
+
matingMidpoint.set(m.id, { x: round5((hubStubX + spouseStubX) / 2), y: round5(dipY) });
|
|
2951
|
+
} else if (ay === by) {
|
|
2952
|
+
const y = ay;
|
|
2953
|
+
elements.push({
|
|
2954
|
+
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2955
|
+
kind: "mating",
|
|
2956
|
+
points: [
|
|
2957
|
+
{ x: round5(lx), y: round5(y) },
|
|
2958
|
+
{ x: round5(rx), y: round5(y) }
|
|
2959
|
+
],
|
|
2960
|
+
consanguineous: m.consanguineous,
|
|
2961
|
+
title
|
|
2962
|
+
});
|
|
2963
|
+
const midX = (lx + rx) / 2;
|
|
2964
|
+
const channelLeft = cxOf(leftId) + nodeHalfWidth(individualById.get(leftId));
|
|
2965
|
+
const channelRight = cxOf(rightId) - nodeHalfWidth(individualById.get(rightId));
|
|
2966
|
+
const barCenter = matingBarCenter.get(m.id);
|
|
2967
|
+
const originX = barCenter !== void 0 && barCenter >= channelLeft && barCenter <= channelRight ? barCenter : midX;
|
|
2968
|
+
matingMidpoint.set(m.id, { x: round5(originX), y: round5(y) });
|
|
2969
|
+
} else {
|
|
2970
|
+
const upId = ay <= by ? aId : bId;
|
|
2971
|
+
const downId = ay <= by ? bId : aId;
|
|
2972
|
+
const upX = cxOf(upId);
|
|
2973
|
+
const upY = cyOf(upId);
|
|
2974
|
+
const downX = cxOf(downId);
|
|
2975
|
+
const downY = cyOf(downId);
|
|
2976
|
+
if (childToSibship.has(downId)) {
|
|
2977
|
+
const upRow = rowOfGeneration.get(individualById.get(upId).generation);
|
|
2978
|
+
const laneY = rowTop[upRow] + rowHeight[upRow] + CORRIDOR2 / 4 + 4;
|
|
2979
|
+
const downRight = downX >= upX;
|
|
2980
|
+
const upExitX = upX + (downRight ? -1 : 1) * (nodeHalfWidth(individualById.get(upId)) + 6);
|
|
2981
|
+
const downFarX = downX + (downRight ? 1 : -1) * (GLYPH_HALF + H_GAP2 / 2);
|
|
2982
|
+
const downSideX = downX + (downRight ? 1 : -1) * GLYPH_HALF;
|
|
2983
|
+
elements.push({
|
|
2984
|
+
edgeId: PED_MATING_ID_BASE + m.id,
|
|
2985
|
+
kind: "mating-elbow",
|
|
2986
|
+
points: [
|
|
2987
|
+
{ x: round5(upX + (downRight ? -GLYPH_HALF : GLYPH_HALF)), y: round5(upY) },
|
|
2988
|
+
{ x: round5(upExitX), y: round5(upY) },
|
|
2989
|
+
{ x: round5(upExitX), y: round5(laneY) },
|
|
2990
|
+
{ x: round5(downFarX), y: round5(laneY) },
|
|
2991
|
+
{ x: round5(downFarX), y: round5(downY) },
|
|
2992
|
+
{ x: round5(downSideX), y: round5(downY) }
|
|
2993
|
+
],
|
|
2994
|
+
consanguineous: m.consanguineous,
|
|
2995
|
+
title
|
|
2996
|
+
});
|
|
2997
|
+
matingMidpoint.set(m.id, { x: round5(downFarX), y: round5(downY) });
|
|
2998
|
+
} else {
|
|
2999
|
+
const elbowX = (upX + downX) / 2;
|
|
3000
|
+
elements.push({
|
|
3001
|
+
edgeId: PED_MATING_ID_BASE + m.id,
|
|
3002
|
+
kind: "mating-elbow",
|
|
3003
|
+
points: [
|
|
3004
|
+
{ x: round5(upX + (downX >= upX ? GLYPH_HALF : -GLYPH_HALF)), y: round5(upY) },
|
|
3005
|
+
{ x: round5(elbowX), y: round5(upY) },
|
|
3006
|
+
{ x: round5(elbowX), y: round5(downY) },
|
|
3007
|
+
{ x: round5(downX + (upX >= downX ? GLYPH_HALF : -GLYPH_HALF)), y: round5(downY) }
|
|
3008
|
+
],
|
|
3009
|
+
consanguineous: m.consanguineous,
|
|
3010
|
+
title
|
|
3011
|
+
});
|
|
3012
|
+
matingMidpoint.set(m.id, { x: round5(elbowX), y: round5(downY) });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
const twinZygositiesUsedSet = /* @__PURE__ */ new Set();
|
|
3017
|
+
const unknownTwinJunctions = [];
|
|
3018
|
+
const bendLaneByRow = /* @__PURE__ */ new Map();
|
|
3019
|
+
for (const s of sibships) {
|
|
3020
|
+
const childIds = s.childIds.filter((id) => individualById.has(id));
|
|
3021
|
+
if (childIds.length === 0) continue;
|
|
3022
|
+
const mid = matingMidpoint.get(s.matingId);
|
|
3023
|
+
const childRow = rowOfGeneration.get(individualById.get(childIds[0]).generation);
|
|
3024
|
+
const barY = rowTop[childRow] - CORRIDOR2 / 2;
|
|
3025
|
+
const lane = bendLaneByRow.get(childRow) ?? 0;
|
|
3026
|
+
bendLaneByRow.set(childRow, lane + 1);
|
|
3027
|
+
const childXs = childIds.map(cxOf);
|
|
3028
|
+
const barLeft = Math.min(...childXs);
|
|
3029
|
+
const barRight = Math.max(...childXs);
|
|
3030
|
+
if (mid !== void 0) {
|
|
3031
|
+
const barCenterX = (barLeft + barRight) / 2;
|
|
3032
|
+
const bendY = barY - 2 - lane % 3 * 3;
|
|
3033
|
+
const descentPoints = Math.abs(mid.x - barCenterX) < 0.01 ? [
|
|
3034
|
+
{ x: round5(mid.x), y: round5(mid.y) },
|
|
3035
|
+
{ x: round5(mid.x), y: round5(barY) }
|
|
3036
|
+
] : [
|
|
3037
|
+
{ x: round5(mid.x), y: round5(mid.y) },
|
|
3038
|
+
{ x: round5(mid.x), y: round5(bendY) },
|
|
3039
|
+
{ x: round5(barCenterX), y: round5(bendY) },
|
|
3040
|
+
{ x: round5(barCenterX), y: round5(barY) }
|
|
3041
|
+
];
|
|
3042
|
+
elements.push({
|
|
3043
|
+
edgeId: PED_DESCENT_ID_BASE + s.id,
|
|
3044
|
+
kind: "descent",
|
|
3045
|
+
points: descentPoints,
|
|
3046
|
+
consanguineous: false,
|
|
3047
|
+
title: titleLabels.sibship
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
if (childIds.length > 1) {
|
|
3051
|
+
elements.push({
|
|
3052
|
+
edgeId: PED_SIBBAR_ID_BASE + s.id,
|
|
3053
|
+
kind: "sibship-bar",
|
|
3054
|
+
points: [
|
|
3055
|
+
{ x: round5(barLeft), y: round5(barY) },
|
|
3056
|
+
{ x: round5(barRight), y: round5(barY) }
|
|
3057
|
+
],
|
|
3058
|
+
consanguineous: false,
|
|
3059
|
+
title: titleLabels.sibship
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
const twinGroupOfChild = /* @__PURE__ */ new Map();
|
|
3063
|
+
s.twinGroups.forEach((tg, ordinal) => {
|
|
3064
|
+
const members = tg.childIds.filter((id) => childIds.includes(id));
|
|
3065
|
+
for (const id of members) twinGroupOfChild.set(id, { ordinal, zygosity: tg.zygosity, members });
|
|
3066
|
+
});
|
|
3067
|
+
const emittedTwinOrdinals = /* @__PURE__ */ new Set();
|
|
3068
|
+
for (const childId of childIds) {
|
|
3069
|
+
const cx = cxOf(childId);
|
|
3070
|
+
const childTop = cyOf(childId) - PED_GLYPH / 2;
|
|
3071
|
+
const tg = twinGroupOfChild.get(childId);
|
|
3072
|
+
if (tg === void 0) {
|
|
3073
|
+
elements.push({
|
|
3074
|
+
edgeId: PED_RISER_ID_BASE + childId,
|
|
3075
|
+
kind: "riser",
|
|
3076
|
+
points: [
|
|
3077
|
+
{ x: round5(cx), y: round5(barY) },
|
|
3078
|
+
{ x: round5(cx), y: round5(childTop) }
|
|
3079
|
+
],
|
|
3080
|
+
consanguineous: false,
|
|
3081
|
+
title: titleLabels.sibship
|
|
3082
|
+
});
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
const memberXs = tg.members.map(cxOf);
|
|
3086
|
+
const junctionX = (Math.min(...memberXs) + Math.max(...memberXs)) / 2;
|
|
3087
|
+
const junctionY = barY + TWIN_JUNCTION_DROP;
|
|
3088
|
+
twinZygositiesUsedSet.add(tg.zygosity);
|
|
3089
|
+
if (!emittedTwinOrdinals.has(tg.ordinal)) {
|
|
3090
|
+
emittedTwinOrdinals.add(tg.ordinal);
|
|
3091
|
+
if (tg.zygosity === "unknown") {
|
|
3092
|
+
unknownTwinJunctions.push({ x: round5(junctionX), y: round5(junctionY) });
|
|
3093
|
+
}
|
|
3094
|
+
elements.push({
|
|
3095
|
+
edgeId: PED_RISER_ID_BASE + childId,
|
|
3096
|
+
// anchored on the first member for a stable id
|
|
3097
|
+
kind: "riser",
|
|
3098
|
+
points: [
|
|
3099
|
+
{ x: round5(junctionX), y: round5(barY) },
|
|
3100
|
+
{ x: round5(junctionX), y: round5(junctionY) }
|
|
3101
|
+
],
|
|
3102
|
+
consanguineous: false,
|
|
3103
|
+
title: titleLabels.twins[tg.zygosity]
|
|
3104
|
+
});
|
|
3105
|
+
if (tg.zygosity === "mz" && memberXs.length >= 2) {
|
|
3106
|
+
const tieY = junctionY + 6;
|
|
3107
|
+
elements.push({
|
|
3108
|
+
edgeId: PED_TWINBAR_ID_BASE + s.id * 100 + tg.ordinal,
|
|
3109
|
+
kind: "twin-bar",
|
|
3110
|
+
points: [
|
|
3111
|
+
{ x: round5(Math.min(...memberXs)), y: round5(tieY) },
|
|
3112
|
+
{ x: round5(Math.max(...memberXs)), y: round5(tieY) }
|
|
3113
|
+
],
|
|
3114
|
+
consanguineous: false,
|
|
3115
|
+
title: titleLabels.twins.mz
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
const horizontalThenDown = Math.abs(cx - junctionX) < 0.01 ? [
|
|
3120
|
+
{ x: round5(cx), y: round5(junctionY) },
|
|
3121
|
+
{ x: round5(cx), y: round5(childTop) }
|
|
3122
|
+
] : [
|
|
3123
|
+
{ x: round5(junctionX), y: round5(junctionY) },
|
|
3124
|
+
{ x: round5(cx), y: round5(junctionY) },
|
|
3125
|
+
{ x: round5(cx), y: round5(childTop) }
|
|
3126
|
+
];
|
|
3127
|
+
elements.push({
|
|
3128
|
+
edgeId: PED_RISER_ID_BASE + childId,
|
|
3129
|
+
kind: "riser",
|
|
3130
|
+
points: horizontalThenDown,
|
|
3131
|
+
consanguineous: false,
|
|
3132
|
+
title: titleLabels.twins[tg.zygosity]
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
let maxX = PADDING5 * 2;
|
|
3137
|
+
let maxY = PADDING5 * 2;
|
|
3138
|
+
for (const n of nodes) {
|
|
3139
|
+
const labelW = n.labelLines.reduce((m, l) => Math.max(m, estimateTextWidth(l, PED_LABEL_FONT)), 0);
|
|
3140
|
+
const half = Math.max(PED_GLYPH, labelW) / 2;
|
|
3141
|
+
maxX = Math.max(maxX, n.cx + half + PADDING5);
|
|
3142
|
+
const labelBottom = n.labelTop + (n.labelLines.length + 1) * PED_LABEL_LINE_H;
|
|
3143
|
+
maxY = Math.max(maxY, labelBottom + PADDING5);
|
|
3144
|
+
}
|
|
3145
|
+
for (const el of elements) {
|
|
3146
|
+
for (const p of el.points) {
|
|
3147
|
+
maxX = Math.max(maxX, p.x + PADDING5);
|
|
3148
|
+
maxY = Math.max(maxY, p.y + PADDING5);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const generations = [];
|
|
3152
|
+
for (let row = 0; row < rowCount; row++) {
|
|
3153
|
+
generations.push({ roman: romanNumeral(row + 1), y: round5(glyphCyOfRow(row)) });
|
|
3154
|
+
}
|
|
3155
|
+
const ZYGOSITY_ORDER = ["mz", "dz", "unknown"];
|
|
3156
|
+
const twinZygositiesUsed = ZYGOSITY_ORDER.filter((z) => twinZygositiesUsedSet.has(z));
|
|
3157
|
+
return {
|
|
3158
|
+
width: Math.ceil(maxX),
|
|
3159
|
+
height: Math.ceil(maxY),
|
|
3160
|
+
nodes,
|
|
3161
|
+
elements,
|
|
3162
|
+
generations,
|
|
3163
|
+
conditionFills,
|
|
3164
|
+
twinZygositiesUsed,
|
|
3165
|
+
unknownTwinJunctions,
|
|
3166
|
+
isolatedIndividualIds
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/pedigree/svg.ts
|
|
3171
|
+
var GLYPH_STROKE3 = "#52525b";
|
|
3172
|
+
var LABEL_FILL4 = "#3f3f46";
|
|
3173
|
+
var EDGE_INK5 = "#71717a";
|
|
3174
|
+
var GLYPH_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="2"`;
|
|
3175
|
+
var CONSANG_GAP = 3;
|
|
3176
|
+
var ZYGOSITIES = ["mz", "dz", "unknown"];
|
|
3177
|
+
var round6 = (n) => Math.round(n * 100) / 100;
|
|
3178
|
+
function glyphOutline(shape, cx, cy, half) {
|
|
3179
|
+
if (shape === "square") {
|
|
3180
|
+
return `<rect x="${round6(cx - half)}" y="${round6(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS2}/>`;
|
|
3181
|
+
}
|
|
3182
|
+
if (shape === "circle") {
|
|
3183
|
+
return `<circle cx="${cx}" cy="${cy}" r="${half}" ${GLYPH_ATTRS2}/>`;
|
|
3184
|
+
}
|
|
3185
|
+
return `<polygon points="${cx},${round6(cy - half)} ${round6(cx + half)},${cy} ${cx},${round6(cy + half)} ${round6(cx - half)},${cy}" ${GLYPH_ATTRS2}/>`;
|
|
3186
|
+
}
|
|
3187
|
+
function glyphVerticalExtentAt(shape, cy, half, dx) {
|
|
3188
|
+
const ax = Math.min(Math.abs(dx), half);
|
|
3189
|
+
if (shape === "square") return [cy - half, cy + half];
|
|
3190
|
+
if (shape === "circle") {
|
|
3191
|
+
const h2 = Math.sqrt(Math.max(0, half * half - ax * ax));
|
|
3192
|
+
return [cy - h2, cy + h2];
|
|
3193
|
+
}
|
|
3194
|
+
const h = half - ax;
|
|
3195
|
+
return [cy - h, cy + h];
|
|
3196
|
+
}
|
|
3197
|
+
function fillPartitions(n, half, inkByCondition) {
|
|
3198
|
+
const ids = n.affectedBy;
|
|
3199
|
+
if (ids.length === 0) return "";
|
|
3200
|
+
const cx = n.cx;
|
|
3201
|
+
const cy = n.cy;
|
|
3202
|
+
const sliceW = half * 2 / ids.length;
|
|
3203
|
+
return ids.map((id, i) => {
|
|
3204
|
+
const left = cx - half + i * sliceW;
|
|
3205
|
+
const SAMPLES = 8;
|
|
3206
|
+
const top = [];
|
|
3207
|
+
const bottom = [];
|
|
3208
|
+
for (let s = 0; s <= SAMPLES; s++) {
|
|
3209
|
+
const x = left + sliceW * s / SAMPLES;
|
|
3210
|
+
const [yt, yb] = glyphVerticalExtentAt(n.shape, cy, half, x - cx);
|
|
3211
|
+
top.push(`${round6(x)},${round6(yt)}`);
|
|
3212
|
+
bottom.push(`${round6(x)},${round6(yb)}`);
|
|
3213
|
+
}
|
|
3214
|
+
const pts = [...top, ...bottom.reverse()].join(" ");
|
|
3215
|
+
const ink = inkByCondition.get(id) ?? GLYPH_STROKE3;
|
|
3216
|
+
return `<polygon points="${pts}" fill="${ink}" stroke="none"/>`;
|
|
3217
|
+
}).join("");
|
|
3218
|
+
}
|
|
3219
|
+
function carrierDot(n) {
|
|
3220
|
+
if (!n.carrier || n.affectedBy.length > 0) return "";
|
|
3221
|
+
return `<circle cx="${n.cx}" cy="${n.cy}" r="4" fill="${GLYPH_STROKE3}" stroke="none"/>`;
|
|
3222
|
+
}
|
|
3223
|
+
function deceasedSlash(n, half) {
|
|
3224
|
+
if (!n.deceased) return "";
|
|
3225
|
+
const ext = half + 4;
|
|
3226
|
+
return `<line x1="${round6(n.cx - ext)}" y1="${round6(n.cy - ext)}" x2="${round6(n.cx + ext)}" y2="${round6(n.cy + ext)}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
|
|
3227
|
+
}
|
|
3228
|
+
function probandArrow(n, half) {
|
|
3229
|
+
if (n.role === null) return "";
|
|
3230
|
+
const tipX = round6(n.cx - half);
|
|
3231
|
+
const tipY = round6(n.cy + half);
|
|
3232
|
+
const tailX = round6(tipX - 16);
|
|
3233
|
+
const tailY = round6(tipY + 16);
|
|
3234
|
+
const filled = n.role === "proband";
|
|
3235
|
+
const fill = filled ? GLYPH_STROKE3 : "transparent";
|
|
3236
|
+
const shaft = `<line x1="${tailX}" y1="${tailY}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
|
|
3237
|
+
const head = `<polygon points="${tipX},${tipY} ${round6(tipX - 8)},${round6(tipY + 2)} ${round6(tipX - 2)},${round6(tipY + 8)}" fill="${fill}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3238
|
+
return shaft + head;
|
|
3239
|
+
}
|
|
3240
|
+
function stillbirthMark(n, half) {
|
|
3241
|
+
if (!n.stillbirth) return "";
|
|
3242
|
+
const y = round6(n.cy + half + PED_ADDRESS_FONT);
|
|
3243
|
+
return `<text x="${round6(n.cx - half - 2)}" y="${y}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" font-weight="bold" fill="${LABEL_FILL4}">SB</text>`;
|
|
3244
|
+
}
|
|
3245
|
+
function nodeSvg2(n, inkByCondition) {
|
|
3246
|
+
const half = n.size / 2;
|
|
3247
|
+
const pieces = [
|
|
3248
|
+
`<title>${xmlEscape(n.title)}</title>`,
|
|
3249
|
+
// Fill partitions sit UNDER the outline so the border stays crisp.
|
|
3250
|
+
fillPartitions(n, half, inkByCondition),
|
|
3251
|
+
glyphOutline(n.shape, n.cx, n.cy, half),
|
|
3252
|
+
carrierDot(n),
|
|
3253
|
+
deceasedSlash(n, half),
|
|
3254
|
+
probandArrow(n, half),
|
|
3255
|
+
stillbirthMark(n, half)
|
|
3256
|
+
];
|
|
3257
|
+
const addressY = round6(n.labelTop + PED_ADDRESS_FONT);
|
|
3258
|
+
pieces.push(
|
|
3259
|
+
`<text x="${n.cx}" y="${addressY}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" fill="${LABEL_FILL4}">${xmlEscape(n.addressLabel)}</text>`
|
|
3260
|
+
);
|
|
3261
|
+
if (n.labelLines.length > 0) {
|
|
3262
|
+
const firstBaseline = round6(n.labelTop + PED_LABEL_LINE_H + 10);
|
|
3263
|
+
const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round6(firstBaseline + i * PED_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
|
|
3264
|
+
pieces.push(
|
|
3265
|
+
`<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" fill="${LABEL_FILL4}">${tspans}</text>`
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
return `<g data-individual-id="p${n.individualId}">${pieces.join("")}</g>`;
|
|
3269
|
+
}
|
|
3270
|
+
function pathData3(points) {
|
|
3271
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
3272
|
+
}
|
|
3273
|
+
function elementSvg3(el) {
|
|
3274
|
+
const pts = el.points;
|
|
3275
|
+
const title = `<title>${xmlEscape(el.title)}</title>`;
|
|
3276
|
+
const stroke = `stroke="${EDGE_INK5}" stroke-width="1.5" stroke-opacity="0.75"`;
|
|
3277
|
+
const draw = (offsetY) => {
|
|
3278
|
+
const shifted = pts.map((p) => ({ x: p.x, y: round6(p.y + offsetY) }));
|
|
3279
|
+
if (shifted.length === 2) {
|
|
3280
|
+
return `<line x1="${shifted[0].x}" y1="${shifted[0].y}" x2="${shifted[1].x}" y2="${shifted[1].y}" ${stroke}/>`;
|
|
3281
|
+
}
|
|
3282
|
+
return `<path d="${pathData3(shifted)}" fill="none" ${stroke}/>`;
|
|
3283
|
+
};
|
|
3284
|
+
const body = el.consanguineous && (el.kind === "mating" || el.kind === "mating-elbow") ? draw(-CONSANG_GAP / 2) + draw(CONSANG_GAP / 2) : draw(0);
|
|
3285
|
+
return `<g data-edge-id="${el.edgeId}">${title}${body}</g>`;
|
|
3286
|
+
}
|
|
3287
|
+
function unknownTwinMarks(layout) {
|
|
3288
|
+
return layout.unknownTwinJunctions.map(
|
|
3289
|
+
(p) => `<text x="${round6(p.x + 6)}" y="${round6(p.y + 4)}" font-family="${FONT_FAMILY}" font-size="12" font-weight="bold" fill="${LABEL_FILL4}">?</text>`
|
|
3290
|
+
).join("");
|
|
3291
|
+
}
|
|
3292
|
+
var MINI_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="1.5"`;
|
|
3293
|
+
function miniShapeSwatch(shape, x, y) {
|
|
3294
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3295
|
+
if (shape === "square") return `<rect x="${round6(cx - 6)}" y="${round6(y - 6)}" width="12" height="12" ${MINI_ATTRS2}/>`;
|
|
3296
|
+
if (shape === "circle") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/>`;
|
|
3297
|
+
return `<polygon points="${cx},${round6(y - 7)} ${round6(cx + 7)},${y} ${cx},${round6(y + 7)} ${round6(cx - 7)},${y}" ${MINI_ATTRS2}/>`;
|
|
3298
|
+
}
|
|
3299
|
+
function miniSwatchCircle(filled, ink, x, y) {
|
|
3300
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3301
|
+
return `<circle cx="${cx}" cy="${y}" r="6" fill="${filled ? ink : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3302
|
+
}
|
|
3303
|
+
function miniCarrierSwatch(x, y) {
|
|
3304
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3305
|
+
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><circle cx="${cx}" cy="${y}" r="2" fill="${GLYPH_STROKE3}" stroke="none"/>`;
|
|
3306
|
+
}
|
|
3307
|
+
function miniDeceasedSwatch(x, y) {
|
|
3308
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3309
|
+
return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><line x1="${round6(cx - 7)}" y1="${round6(y - 7)}" x2="${round6(cx + 7)}" y2="${round6(y + 7)}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
|
|
3310
|
+
}
|
|
3311
|
+
function miniArrowSwatch(filled, x, y) {
|
|
3312
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3313
|
+
const tipX = round6(cx - 2);
|
|
3314
|
+
const tipY = round6(y + 2);
|
|
3315
|
+
return `<line x1="${round6(tipX - 8)}" y1="${round6(tipY + 8)}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/><polygon points="${tipX},${tipY} ${round6(tipX - 5)},${round6(tipY + 1)} ${round6(tipX - 1)},${round6(tipY + 5)}" fill="${filled ? GLYPH_STROKE3 : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1"/>`;
|
|
3316
|
+
}
|
|
3317
|
+
function miniConsanguineousSwatch(x, y) {
|
|
3318
|
+
const x1 = round6(x + 2);
|
|
3319
|
+
const x2 = round6(x + LEGEND_SWATCH_W - 2);
|
|
3320
|
+
return `<line x1="${x1}" y1="${round6(y - 1.5)}" x2="${x2}" y2="${round6(y - 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${x1}" y1="${round6(y + 1.5)}" x2="${x2}" y2="${round6(y + 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3321
|
+
}
|
|
3322
|
+
function miniTwinSwatch(zygosity, x, y) {
|
|
3323
|
+
const cx = round6(x + LEGEND_SWATCH_W / 2);
|
|
3324
|
+
const apexY = round6(y - 6);
|
|
3325
|
+
const baseY = round6(y + 6);
|
|
3326
|
+
const left = round6(cx - 6);
|
|
3327
|
+
const right = round6(cx + 6);
|
|
3328
|
+
const stub = `<line x1="${cx}" y1="${apexY}" x2="${cx}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${right}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${left}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${right}" y1="${y}" x2="${right}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3329
|
+
if (zygosity === "mz") {
|
|
3330
|
+
return stub + `<line x1="${left}" y1="${round6(y + 3)}" x2="${right}" y2="${round6(y + 3)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
|
|
3331
|
+
}
|
|
3332
|
+
if (zygosity === "unknown") {
|
|
3333
|
+
return stub + `<text x="${round6(cx + 8)}" y="${round6(y + 3)}" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL4}">?</text>`;
|
|
3334
|
+
}
|
|
3335
|
+
return stub;
|
|
3336
|
+
}
|
|
3337
|
+
function pedigreeLayoutSvg(layout, opts = {}) {
|
|
3338
|
+
const labels = opts.labels ?? PEDIGREE_SVG_LABELS_EN;
|
|
3339
|
+
const inkByCondition = new Map(layout.conditionFills.map((c) => [c.id, c.ink]));
|
|
3340
|
+
const parts = [];
|
|
3341
|
+
for (const el of layout.elements) parts.push(elementSvg3(el));
|
|
3342
|
+
parts.push(unknownTwinMarks(layout));
|
|
3343
|
+
for (const n of layout.nodes) parts.push(nodeSvg2(n, inkByCondition));
|
|
3344
|
+
for (const g of layout.generations) {
|
|
3345
|
+
parts.push(
|
|
3346
|
+
`<text x="${round6(16)}" y="${round6(g.y + PED_LABEL_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" font-weight="bold" fill="${LABEL_FILL4}">${xmlEscape(g.roman)}</text>`
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
let width = layout.width;
|
|
3350
|
+
let height = layout.height;
|
|
3351
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
3352
|
+
const entries = [];
|
|
3353
|
+
const shapesUsed = new Set(layout.nodes.map((n) => n.shape));
|
|
3354
|
+
for (const shape of ["square", "circle", "diamond"]) {
|
|
3355
|
+
if (!shapesUsed.has(shape)) continue;
|
|
3356
|
+
entries.push({ swatch: (x, y) => miniShapeSwatch(shape, x, y), label: labels.shapes[shape] });
|
|
3357
|
+
}
|
|
3358
|
+
for (const c of layout.conditionFills) {
|
|
3359
|
+
entries.push({ swatch: (x, y) => miniSwatchCircle(true, c.ink, x, y), label: c.label });
|
|
3360
|
+
}
|
|
3361
|
+
if (layout.nodes.some((n) => n.affectedBy.length === 0)) {
|
|
3362
|
+
entries.push({ swatch: (x, y) => miniSwatchCircle(false, GLYPH_STROKE3, x, y), label: labels.unaffected });
|
|
3363
|
+
}
|
|
3364
|
+
if (layout.nodes.some((n) => n.carrier && n.affectedBy.length === 0)) {
|
|
3365
|
+
entries.push({ swatch: miniCarrierSwatch, label: labels.carrier });
|
|
3366
|
+
}
|
|
3367
|
+
if (layout.nodes.some((n) => n.deceased)) {
|
|
3368
|
+
entries.push({ swatch: miniDeceasedSwatch, label: labels.deceased });
|
|
3369
|
+
}
|
|
3370
|
+
if (layout.nodes.some((n) => n.role === "proband")) {
|
|
3371
|
+
entries.push({ swatch: (x, y) => miniArrowSwatch(true, x, y), label: labels.proband });
|
|
3372
|
+
}
|
|
3373
|
+
if (layout.nodes.some((n) => n.role === "consultand")) {
|
|
3374
|
+
entries.push({ swatch: (x, y) => miniArrowSwatch(false, x, y), label: labels.consultand });
|
|
3375
|
+
}
|
|
3376
|
+
if (layout.nodes.some((n) => n.stillbirth)) {
|
|
3377
|
+
entries.push({ swatch: () => "", label: labels.stillbirth });
|
|
3378
|
+
}
|
|
3379
|
+
if (layout.elements.some((el) => el.consanguineous)) {
|
|
3380
|
+
entries.push({ swatch: miniConsanguineousSwatch, label: labels.consanguineous });
|
|
3381
|
+
}
|
|
3382
|
+
const twinsUsed = new Set(layout.twinZygositiesUsed);
|
|
3383
|
+
for (const z of ZYGOSITIES) {
|
|
3384
|
+
if (!twinsUsed.has(z)) continue;
|
|
3385
|
+
entries.push({ swatch: (x, y) => miniTwinSwatch(z, x, y), label: labels.twins[z] });
|
|
3386
|
+
}
|
|
3387
|
+
if (layout.isolatedIndividualIds.length > 0) {
|
|
3388
|
+
entries.push({ swatch: () => "", label: labels.isolated });
|
|
3389
|
+
}
|
|
3390
|
+
const block = legendBlock(entries, layout.height);
|
|
3391
|
+
if (block.svg !== "") {
|
|
3392
|
+
parts.push(block.svg);
|
|
3393
|
+
width = Math.max(width, block.width);
|
|
3394
|
+
height = block.height;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
const w = Math.ceil(width);
|
|
3398
|
+
const h = Math.ceil(height);
|
|
3399
|
+
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>`;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// src/pedigree/render.ts
|
|
3403
|
+
function pedigreeSvg(input, opts = {}) {
|
|
3404
|
+
const layout = computePedigreeLayout(input, {
|
|
3405
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
3406
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
3407
|
+
});
|
|
3408
|
+
const svg = pedigreeLayoutSvg(layout, {
|
|
3409
|
+
...opts.legend === false ? { legend: false } : {},
|
|
3410
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
3411
|
+
});
|
|
3412
|
+
return { svg, layout };
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
// src/phylo/labels.ts
|
|
3416
|
+
var PHYLO_TITLE_LABELS_EN = {
|
|
3417
|
+
branchLength: "branch length",
|
|
3418
|
+
support: "support",
|
|
3419
|
+
clade: "clade",
|
|
3420
|
+
tip: "tip",
|
|
3421
|
+
root: "root"
|
|
3422
|
+
};
|
|
3423
|
+
var PHYLO_SVG_LABELS_EN = {
|
|
3424
|
+
support: "Support value (bootstrap/posterior)",
|
|
3425
|
+
scaleBar: "Scale: substitutions per site",
|
|
3426
|
+
alignedTip: "Aligned tip (true position dotted)",
|
|
3427
|
+
ariaLabel: {
|
|
3428
|
+
cladogram: "Phylogenetic tree (cladogram)",
|
|
3429
|
+
phylogram: "Phylogenetic tree (phylogram)"
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
// src/phylo/validate.ts
|
|
3434
|
+
var PhyloValidationError = class extends Error {
|
|
3435
|
+
issues;
|
|
3436
|
+
constructor(issues) {
|
|
3437
|
+
super(`invalid phylo tree: ${issues.map((i) => i.message).join("; ")}`);
|
|
3438
|
+
this.name = "PhyloValidationError";
|
|
3439
|
+
this.issues = issues;
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
function sortIssues3(issues) {
|
|
3443
|
+
const unique = /* @__PURE__ */ new Map();
|
|
3444
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
3445
|
+
return [...unique.values()].sort(
|
|
3446
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
function phyloIssues(input) {
|
|
3450
|
+
if (input.nodes.length === 0 && input.edges.length === 0) return [];
|
|
3451
|
+
const issues = [];
|
|
3452
|
+
const push = (code, message) => {
|
|
3453
|
+
issues.push({ code, message });
|
|
3454
|
+
};
|
|
3455
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
3456
|
+
const dupNodeIds = /* @__PURE__ */ new Set();
|
|
3457
|
+
for (const n of input.nodes) {
|
|
3458
|
+
if (nodeById.has(n.id)) dupNodeIds.add(n.id);
|
|
3459
|
+
else nodeById.set(n.id, n);
|
|
3460
|
+
}
|
|
3461
|
+
for (const id of [...dupNodeIds].sort((a, b) => a - b)) {
|
|
3462
|
+
push("duplicate-id", `duplicate node id ${id}`);
|
|
3463
|
+
}
|
|
3464
|
+
const dupEdgeIds = /* @__PURE__ */ new Set();
|
|
3465
|
+
const seenEdgeIds = /* @__PURE__ */ new Set();
|
|
3466
|
+
for (const e of input.edges) {
|
|
3467
|
+
if (seenEdgeIds.has(e.id)) dupEdgeIds.add(e.id);
|
|
3468
|
+
else seenEdgeIds.add(e.id);
|
|
3469
|
+
}
|
|
3470
|
+
for (const id of [...dupEdgeIds].sort((a, b) => a - b)) {
|
|
3471
|
+
push("duplicate-id", `duplicate edge id ${id}`);
|
|
3472
|
+
}
|
|
3473
|
+
if (issues.length > 0) {
|
|
3474
|
+
return sortIssues3(issues);
|
|
3475
|
+
}
|
|
3476
|
+
if (!nodeById.has(input.rootId)) {
|
|
3477
|
+
push("unknown-root", `rootId ${input.rootId} is not a declared node`);
|
|
3478
|
+
}
|
|
3479
|
+
const incomingByChild = /* @__PURE__ */ new Map();
|
|
3480
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
3481
|
+
const knownParent = nodeById.has(e.parentId);
|
|
3482
|
+
const knownChild = nodeById.has(e.childId);
|
|
3483
|
+
if (!knownParent) push("unknown-endpoint", `edge ${e.id} parentId ${e.parentId} is not a declared node`);
|
|
3484
|
+
if (!knownChild) push("unknown-endpoint", `edge ${e.id} childId ${e.childId} is not a declared node`);
|
|
3485
|
+
if (e.length !== null && (!Number.isFinite(e.length) || e.length < 0)) {
|
|
3486
|
+
push("negative-length", `edge ${e.id} has non-finite or negative length ${e.length}`);
|
|
3487
|
+
}
|
|
3488
|
+
if (knownParent && knownChild) {
|
|
3489
|
+
const arr = incomingByChild.get(e.childId) ?? [];
|
|
3490
|
+
arr.push(e.parentId);
|
|
3491
|
+
incomingByChild.set(e.childId, arr);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const childIdsWithEdges = /* @__PURE__ */ new Set();
|
|
3495
|
+
for (const e of input.edges) childIdsWithEdges.add(e.childId);
|
|
3496
|
+
for (const [childId, parents] of [...incomingByChild.entries()].sort((a, b) => a[0] - b[0])) {
|
|
3497
|
+
if (parents.length > 1) {
|
|
3498
|
+
push("multiple-parents", `node ${childId} has ${parents.length} parents`);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
const hasChildren = /* @__PURE__ */ new Set();
|
|
3502
|
+
for (const e of input.edges) {
|
|
3503
|
+
if (nodeById.has(e.parentId)) hasChildren.add(e.parentId);
|
|
3504
|
+
}
|
|
3505
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
3506
|
+
if (n.isTip === true && hasChildren.has(n.id)) {
|
|
3507
|
+
push("tip-with-children", `node ${n.id} is declared a tip but has children`);
|
|
3508
|
+
} else if (n.isTip === false && !hasChildren.has(n.id)) {
|
|
3509
|
+
push("tip-with-children", `node ${n.id} is declared internal but has no children`);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
|
|
3513
|
+
"duplicate-id",
|
|
3514
|
+
"unknown-endpoint",
|
|
3515
|
+
"unknown-root",
|
|
3516
|
+
"multiple-parents"
|
|
3517
|
+
]);
|
|
3518
|
+
if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
|
|
3519
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3520
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
3521
|
+
const arr = childrenOf.get(e.parentId) ?? [];
|
|
3522
|
+
arr.push(e.childId);
|
|
3523
|
+
childrenOf.set(e.parentId, arr);
|
|
3524
|
+
}
|
|
3525
|
+
const color = /* @__PURE__ */ new Map();
|
|
3526
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
3527
|
+
const dfs = (start) => {
|
|
3528
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
3529
|
+
color.set(start, 1);
|
|
3530
|
+
while (stack.length > 0) {
|
|
3531
|
+
const frame = stack[stack.length - 1];
|
|
3532
|
+
const children = childrenOf.get(frame.id) ?? [];
|
|
3533
|
+
if (frame.nextChild >= children.length) {
|
|
3534
|
+
color.set(frame.id, 2);
|
|
3535
|
+
stack.pop();
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
const child = children[frame.nextChild];
|
|
3539
|
+
frame.nextChild += 1;
|
|
3540
|
+
const c = color.get(child) ?? 0;
|
|
3541
|
+
if (c === 1) {
|
|
3542
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
3543
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
3544
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
3545
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
3546
|
+
const key = rotated.join(">");
|
|
3547
|
+
if (!seenCycles.has(key)) {
|
|
3548
|
+
seenCycles.add(key);
|
|
3549
|
+
push("cycle", `cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`);
|
|
3550
|
+
}
|
|
3551
|
+
} else if (c === 0) {
|
|
3552
|
+
color.set(child, 1);
|
|
3553
|
+
stack.push({ id: child, nextChild: 0 });
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
};
|
|
3557
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
3558
|
+
if ((color.get(n.id) ?? 0) === 0) dfs(n.id);
|
|
3559
|
+
}
|
|
3560
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
3561
|
+
const queue = [input.rootId];
|
|
3562
|
+
while (queue.length > 0) {
|
|
3563
|
+
const id = queue.shift();
|
|
3564
|
+
if (reachable.has(id)) continue;
|
|
3565
|
+
reachable.add(id);
|
|
3566
|
+
for (const childId of childrenOf.get(id) ?? []) queue.push(childId);
|
|
3567
|
+
}
|
|
3568
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
3569
|
+
if (!reachable.has(n.id)) push("disconnected-node", `node ${n.id} is unreachable from the root`);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
return sortIssues3(issues);
|
|
3573
|
+
}
|
|
3574
|
+
function validatePhylo(input) {
|
|
3575
|
+
const issues = phyloIssues(input);
|
|
3576
|
+
if (issues.length > 0) throw new PhyloValidationError(issues);
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// src/phylo/layout.ts
|
|
3580
|
+
var PHYLO_LABEL_FONT = 12;
|
|
3581
|
+
var PHYLO_ROW_SLOT = 22;
|
|
3582
|
+
var PHYLO_SUPPORT_FONT = 10;
|
|
3583
|
+
var PHYLO_LABEL_GAP = 8;
|
|
3584
|
+
var PADDING6 = 32;
|
|
3585
|
+
var PHYLO_LEVEL_W = 72;
|
|
3586
|
+
var PHYLO_TIP_R = 2.5;
|
|
3587
|
+
var SUPPORT_GAP = 4;
|
|
3588
|
+
var SCALEBAR_ZONE = 34;
|
|
3589
|
+
var MIN_DRAW_W = PHYLO_LEVEL_W;
|
|
3590
|
+
var PHYLO_ZERO_NUDGE = 1;
|
|
3591
|
+
var PHYLO_CLADEBAR_ID_BASE = 1e6;
|
|
3592
|
+
var PHYLO_BRANCH_ID_BASE = 2e6;
|
|
3593
|
+
var PHYLO_EXTENSION_ID_BASE = 3e6;
|
|
3594
|
+
var PHYLO_ROOTSTUB_ID_BASE = 4e6;
|
|
3595
|
+
var PHYLO_SCALEBAR_ID = 5e6;
|
|
3596
|
+
var round7 = (n) => Math.round(n * 100) / 100;
|
|
3597
|
+
function niceScaleStep(maxLen) {
|
|
3598
|
+
if (!(maxLen > 0)) return 0;
|
|
3599
|
+
const target = maxLen / 4;
|
|
3600
|
+
const exp = Math.floor(Math.log10(target));
|
|
3601
|
+
const pow = Math.pow(10, exp);
|
|
3602
|
+
const tol = target * 1e-9;
|
|
3603
|
+
for (const mult of [5, 2, 1]) {
|
|
3604
|
+
const step = mult * pow;
|
|
3605
|
+
if (step <= target + tol) return step;
|
|
3606
|
+
}
|
|
3607
|
+
return 5 * Math.pow(10, exp - 1);
|
|
3608
|
+
}
|
|
3609
|
+
function trimNumber(n) {
|
|
3610
|
+
if (Number.isInteger(n)) return String(n);
|
|
3611
|
+
return String(Number(n.toPrecision(6))).replace(/0+$/, "").replace(/\.$/, "");
|
|
3612
|
+
}
|
|
3613
|
+
function nodeTitle(node, isTip, isRoot, inLength, labels) {
|
|
3614
|
+
if (node.title !== void 0) return node.title;
|
|
3615
|
+
const role = isRoot ? labels.root : isTip ? labels.tip : labels.clade;
|
|
3616
|
+
const head = node.label === "" ? role : node.label;
|
|
3617
|
+
const parts = [head];
|
|
3618
|
+
if (inLength !== null) parts.push(`${labels.branchLength}: ${trimNumber(inLength)}`);
|
|
3619
|
+
if (!isTip && node.support !== void 0 && node.support !== null) {
|
|
3620
|
+
parts.push(`${labels.support}: ${trimNumber(node.support)}`);
|
|
3621
|
+
}
|
|
3622
|
+
return parts.join(" \xB7 ");
|
|
3623
|
+
}
|
|
3624
|
+
function computePhyloLayout(input, opts = {}) {
|
|
3625
|
+
const mode = opts.mode ?? "cladogram";
|
|
3626
|
+
if (input.nodes.length === 0 && input.edges.length === 0) {
|
|
3627
|
+
return { width: PADDING6 * 2, height: PADDING6 * 2, mode, nodes: [], elements: [], scaleBar: null, showSupport: false };
|
|
3628
|
+
}
|
|
3629
|
+
validatePhylo(input);
|
|
3630
|
+
const titleLabels = opts.titleLabels ?? PHYLO_TITLE_LABELS_EN;
|
|
3631
|
+
const alignTips = opts.alignTips ?? mode === "cladogram";
|
|
3632
|
+
const showSupport = opts.showSupport ?? false;
|
|
3633
|
+
const wantScaleBar = (opts.scaleBar ?? true) && mode === "phylogram";
|
|
3634
|
+
const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
|
|
3635
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3636
|
+
const inLengthByChild = /* @__PURE__ */ new Map();
|
|
3637
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
3638
|
+
(childrenOf.get(e.parentId) ?? childrenOf.set(e.parentId, []).get(e.parentId)).push(e.childId);
|
|
3639
|
+
inLengthByChild.set(e.childId, e.length);
|
|
3640
|
+
}
|
|
3641
|
+
const allWork = [];
|
|
3642
|
+
const build = (nodeId, depth, dist) => {
|
|
3643
|
+
const node = nodeById.get(nodeId);
|
|
3644
|
+
const inLength = inLengthByChild.get(nodeId) ?? null;
|
|
3645
|
+
const work = { node, children: [], depth, dist, inLength, cx: 0, cy: 0 };
|
|
3646
|
+
allWork.push(work);
|
|
3647
|
+
for (const childId of childrenOf.get(nodeId) ?? []) {
|
|
3648
|
+
const childLen = inLengthByChild.get(childId);
|
|
3649
|
+
const add = childLen === null || childLen === void 0 ? 0 : Math.max(0, childLen);
|
|
3650
|
+
work.children.push(build(childId, depth + 1, dist + add));
|
|
3651
|
+
}
|
|
3652
|
+
return work;
|
|
3653
|
+
};
|
|
3654
|
+
const root = build(input.rootId, 0, 0);
|
|
3655
|
+
const isTip = (w) => w.children.length === 0;
|
|
3656
|
+
const top = PADDING6;
|
|
3657
|
+
let tipIndex = 0;
|
|
3658
|
+
const assignY = (w) => {
|
|
3659
|
+
if (isTip(w)) {
|
|
3660
|
+
w.cy = top + tipIndex * PHYLO_ROW_SLOT + PHYLO_ROW_SLOT / 2;
|
|
3661
|
+
tipIndex += 1;
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
for (const c of w.children) assignY(c);
|
|
3665
|
+
const ys = w.children.map((c) => c.cy);
|
|
3666
|
+
w.cy = (Math.min(...ys) + Math.max(...ys)) / 2;
|
|
3667
|
+
};
|
|
3668
|
+
assignY(root);
|
|
3669
|
+
const tipCount = tipIndex;
|
|
3670
|
+
const maxDepth = allWork.reduce((m, w) => Math.max(m, w.depth), 0);
|
|
3671
|
+
const maxDist = allWork.reduce((m, w) => Math.max(m, w.dist), 0);
|
|
3672
|
+
const supportNodes = allWork.filter(
|
|
3673
|
+
(w) => !isTip(w) && w.node.support !== void 0 && w.node.support !== null
|
|
3674
|
+
);
|
|
3675
|
+
const effectiveShowSupport = showSupport && supportNodes.length > 0;
|
|
3676
|
+
const supportMargin = effectiveShowSupport ? supportNodes.reduce(
|
|
3677
|
+
(m, w) => Math.max(m, estimateTextWidth(trimNumber(w.node.support), PHYLO_SUPPORT_FONT)),
|
|
3678
|
+
0
|
|
3679
|
+
) + SUPPORT_GAP : 0;
|
|
3680
|
+
const padLeft = PADDING6 + supportMargin;
|
|
3681
|
+
const tipLabelW = allWork.filter(isTip).reduce((m, w) => Math.max(m, estimateTextWidth(clampLabel(w.node.label, opts.maxLabelChars), PHYLO_LABEL_FONT)), 0);
|
|
3682
|
+
const labelReserve = (tipCount > 0 ? PHYLO_LABEL_GAP + tipLabelW : 0) + PADDING6;
|
|
3683
|
+
const phylogramScaled = mode === "phylogram" && maxDist > 0;
|
|
3684
|
+
const drawW = Math.max(MIN_DRAW_W, maxDepth * PHYLO_LEVEL_W);
|
|
3685
|
+
const scale = phylogramScaled ? drawW / maxDist : 0;
|
|
3686
|
+
const xOf = (w) => {
|
|
3687
|
+
if (phylogramScaled) return padLeft + w.dist * scale;
|
|
3688
|
+
return padLeft + w.depth * PHYLO_LEVEL_W;
|
|
3689
|
+
};
|
|
3690
|
+
for (const w of allWork) w.cx = xOf(w);
|
|
3691
|
+
if (phylogramScaled) {
|
|
3692
|
+
const nudge = (w) => {
|
|
3693
|
+
for (const c of w.children) {
|
|
3694
|
+
if (!isTip(c) && c.cx <= w.cx + 1e-6) c.cx = w.cx + PHYLO_ZERO_NUDGE;
|
|
3695
|
+
nudge(c);
|
|
3696
|
+
}
|
|
3697
|
+
};
|
|
3698
|
+
nudge(root);
|
|
3699
|
+
}
|
|
3700
|
+
const tipsAligned = alignTips;
|
|
3701
|
+
const maxTipX = allWork.filter(isTip).reduce((m, w) => Math.max(m, w.cx), padLeft);
|
|
3702
|
+
const alignX = phylogramScaled ? maxTipX : padLeft + maxDepth * PHYLO_LEVEL_W;
|
|
3703
|
+
const tipDrawX = (w) => tipsAligned ? alignX : w.cx;
|
|
3704
|
+
const rightmost = tipsAligned ? alignX : allWork.reduce((m, w) => Math.max(m, w.cx), padLeft);
|
|
3705
|
+
const treeBottom = top + (tipCount > 0 ? tipCount * PHYLO_ROW_SLOT : PHYLO_ROW_SLOT);
|
|
3706
|
+
const scaleBarPx = wantScaleBar && maxDist > 0 ? niceScaleStep(maxDist) * scale : 0;
|
|
3707
|
+
const hasScaleBar = wantScaleBar && maxDist > 0 && scaleBarPx > 0;
|
|
3708
|
+
const width = Math.ceil(rightmost + labelReserve);
|
|
3709
|
+
const height = Math.ceil(treeBottom + (hasScaleBar ? SCALEBAR_ZONE : 0) + PADDING6 / 2);
|
|
3710
|
+
const nodes = [];
|
|
3711
|
+
const elements = [];
|
|
3712
|
+
const branchTitle = (w) => {
|
|
3713
|
+
if (w.inLength !== null) return `${titleLabels.branchLength}: ${trimNumber(w.inLength)}`;
|
|
3714
|
+
return titleLabels.branchLength;
|
|
3715
|
+
};
|
|
3716
|
+
const ROOT_STUB = 14;
|
|
3717
|
+
const rootDrawX = isTip(root) ? tipDrawX(root) : root.cx;
|
|
3718
|
+
elements.push({
|
|
3719
|
+
edgeId: PHYLO_ROOTSTUB_ID_BASE + root.node.id,
|
|
3720
|
+
kind: "root-stub",
|
|
3721
|
+
points: [
|
|
3722
|
+
{ x: round7(Math.max(PADDING6 / 2, rootDrawX - ROOT_STUB)), y: round7(root.cy) },
|
|
3723
|
+
{ x: round7(rootDrawX), y: round7(root.cy) }
|
|
3724
|
+
],
|
|
3725
|
+
dotted: false,
|
|
3726
|
+
length: null,
|
|
3727
|
+
title: titleLabels.root
|
|
3728
|
+
});
|
|
3729
|
+
const emit = (w) => {
|
|
3730
|
+
const tip = isTip(w);
|
|
3731
|
+
const isRoot = w === root;
|
|
3732
|
+
const drawX = tip ? tipDrawX(w) : w.cx;
|
|
3733
|
+
const support = w.node.support === void 0 ? null : w.node.support;
|
|
3734
|
+
const labelLine = tip && w.node.label !== "" ? [clampLabel(w.node.label, opts.maxLabelChars)] : [];
|
|
3735
|
+
nodes.push({
|
|
3736
|
+
nodeId: w.node.id,
|
|
3737
|
+
isTip: tip,
|
|
3738
|
+
cx: round7(drawX),
|
|
3739
|
+
cy: round7(w.cy),
|
|
3740
|
+
support,
|
|
3741
|
+
labelLines: labelLine,
|
|
3742
|
+
labelLeft: round7(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
|
|
3743
|
+
depth: w.depth,
|
|
3744
|
+
title: nodeTitle(w.node, tip, isRoot, w.inLength, titleLabels)
|
|
3745
|
+
});
|
|
3746
|
+
if (tip && tipsAligned && w.cx < alignX - 1e-6) {
|
|
3747
|
+
elements.push({
|
|
3748
|
+
edgeId: PHYLO_EXTENSION_ID_BASE + w.node.id,
|
|
3749
|
+
kind: "extension",
|
|
3750
|
+
points: [
|
|
3751
|
+
{ x: round7(w.cx), y: round7(w.cy) },
|
|
3752
|
+
{ x: round7(alignX), y: round7(w.cy) }
|
|
3753
|
+
],
|
|
3754
|
+
dotted: true,
|
|
3755
|
+
length: null,
|
|
3756
|
+
title: titleLabels.tip
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
if (w.children.length === 0) return;
|
|
3760
|
+
const childYs = w.children.map((c) => c.cy);
|
|
3761
|
+
const barTop = Math.min(...childYs);
|
|
3762
|
+
const barBottom = Math.max(...childYs);
|
|
3763
|
+
if (barBottom - barTop > 1e-6) {
|
|
3764
|
+
elements.push({
|
|
3765
|
+
edgeId: PHYLO_CLADEBAR_ID_BASE + w.node.id,
|
|
3766
|
+
kind: "clade-bar",
|
|
3767
|
+
points: [
|
|
3768
|
+
{ x: round7(w.cx), y: round7(barTop) },
|
|
3769
|
+
{ x: round7(w.cx), y: round7(barBottom) }
|
|
3770
|
+
],
|
|
3771
|
+
dotted: false,
|
|
3772
|
+
length: null,
|
|
3773
|
+
title: isRoot ? titleLabels.root : titleLabels.clade
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
for (const c of w.children) {
|
|
3777
|
+
elements.push({
|
|
3778
|
+
edgeId: PHYLO_BRANCH_ID_BASE + c.node.id,
|
|
3779
|
+
kind: "branch",
|
|
3780
|
+
points: [
|
|
3781
|
+
{ x: round7(w.cx), y: round7(c.cy) },
|
|
3782
|
+
{ x: round7(c.cx), y: round7(c.cy) }
|
|
3783
|
+
],
|
|
3784
|
+
dotted: false,
|
|
3785
|
+
length: c.inLength,
|
|
3786
|
+
title: branchTitle(c)
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
for (const c of w.children) emit(c);
|
|
3790
|
+
};
|
|
3791
|
+
emit(root);
|
|
3792
|
+
let scaleBar = null;
|
|
3793
|
+
if (hasScaleBar) {
|
|
3794
|
+
const stepLen = niceScaleStep(maxDist);
|
|
3795
|
+
const barX = padLeft;
|
|
3796
|
+
const barY = treeBottom + SCALEBAR_ZONE / 2;
|
|
3797
|
+
scaleBar = {
|
|
3798
|
+
length: stepLen,
|
|
3799
|
+
x: round7(barX),
|
|
3800
|
+
y: round7(barY),
|
|
3801
|
+
pxLength: round7(scaleBarPx),
|
|
3802
|
+
valueLabel: trimNumber(stepLen)
|
|
3803
|
+
};
|
|
3804
|
+
elements.push({
|
|
3805
|
+
edgeId: PHYLO_SCALEBAR_ID,
|
|
3806
|
+
kind: "scale-bar",
|
|
3807
|
+
points: [
|
|
3808
|
+
{ x: round7(barX), y: round7(barY) },
|
|
3809
|
+
{ x: round7(barX + scaleBarPx), y: round7(barY) }
|
|
3810
|
+
],
|
|
3811
|
+
dotted: false,
|
|
3812
|
+
length: stepLen,
|
|
3813
|
+
title: `${titleLabels.branchLength}: ${trimNumber(stepLen)}`
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
return { width, height, mode, nodes, elements, scaleBar, showSupport: effectiveShowSupport };
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// src/phylo/svg.ts
|
|
3820
|
+
var GLYPH_STROKE4 = "#52525b";
|
|
3821
|
+
var LABEL_FILL5 = "#3f3f46";
|
|
3822
|
+
var EDGE_INK6 = "#71717a";
|
|
3823
|
+
var round8 = (n) => Math.round(n * 100) / 100;
|
|
3824
|
+
function nodeSvg3(n, showSupport) {
|
|
3825
|
+
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
3826
|
+
pieces.push(`<circle cx="${n.cx}" cy="${n.cy}" r="2.5" fill="${GLYPH_STROKE4}"/>`);
|
|
3827
|
+
if (n.labelLines.length > 0) {
|
|
3828
|
+
pieces.push(
|
|
3829
|
+
`<text x="${n.labelLeft}" y="${round8(n.cy + PHYLO_LABEL_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_LABEL_FONT}" fill="${LABEL_FILL5}">${xmlEscape(n.labelLines[0])}</text>`
|
|
3830
|
+
);
|
|
3831
|
+
}
|
|
3832
|
+
if (showSupport && !n.isTip && n.support !== null) {
|
|
3833
|
+
pieces.push(
|
|
3834
|
+
`<text x="${round8(n.cx - 4)}" y="${round8(n.cy - 3)}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(String(n.support))}</text>`
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
return `<g data-node-id="n${n.nodeId}">${pieces.join("")}</g>`;
|
|
3838
|
+
}
|
|
3839
|
+
function elementSvg4(el) {
|
|
3840
|
+
const a = el.points[0];
|
|
3841
|
+
const b = el.points[1];
|
|
3842
|
+
const dash = el.dotted ? ` stroke-dasharray="2,3"` : "";
|
|
3843
|
+
const opacity = el.kind === "extension" ? "0.5" : "0.75";
|
|
3844
|
+
return `<g data-edge-id="${el.edgeId}"><title>${xmlEscape(el.title)}</title><line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="${opacity}"${dash}/></g>`;
|
|
3845
|
+
}
|
|
3846
|
+
function scaleBarLabelSvg(layout) {
|
|
3847
|
+
const bar = layout.scaleBar;
|
|
3848
|
+
if (bar === null) return "";
|
|
3849
|
+
const tick = (x) => `<line x1="${x}" y1="${round8(bar.y - 3)}" x2="${x}" y2="${round8(bar.y + 3)}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
3850
|
+
return tick(bar.x) + tick(round8(bar.x + bar.pxLength)) + `<text x="${round8(bar.x + bar.pxLength / 2)}" y="${round8(bar.y - 6)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(bar.valueLabel)}</text>`;
|
|
3851
|
+
}
|
|
3852
|
+
function supportSwatch(x, y) {
|
|
3853
|
+
const cx = round8(x + LEGEND_SWATCH_W / 2);
|
|
3854
|
+
return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE4}"/><text x="${round8(cx + 5)}" y="${round8(y + PHYLO_SUPPORT_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">95</text>`;
|
|
3855
|
+
}
|
|
3856
|
+
function scaleSwatch(x, y) {
|
|
3857
|
+
return `<line x1="${round8(x)}" y1="${y}" x2="${round8(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
3858
|
+
}
|
|
3859
|
+
function alignedTipSwatch(x, y) {
|
|
3860
|
+
return `<line x1="${round8(x)}" y1="${y}" x2="${round8(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="2,3"/>`;
|
|
3861
|
+
}
|
|
3862
|
+
function phyloLayoutSvg(layout, opts = {}) {
|
|
3863
|
+
const labels = opts.labels ?? PHYLO_SVG_LABELS_EN;
|
|
3864
|
+
const showSupport = layout.showSupport;
|
|
3865
|
+
const parts = [];
|
|
3866
|
+
for (const el of layout.elements) parts.push(elementSvg4(el));
|
|
3867
|
+
parts.push(scaleBarLabelSvg(layout));
|
|
3868
|
+
for (const n of layout.nodes) parts.push(nodeSvg3(n, showSupport));
|
|
3869
|
+
let width = layout.width;
|
|
3870
|
+
let height = layout.height;
|
|
3871
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
3872
|
+
const entries = [];
|
|
3873
|
+
if (showSupport) entries.push({ swatch: supportSwatch, label: labels.support });
|
|
3874
|
+
if (layout.scaleBar !== null) entries.push({ swatch: scaleSwatch, label: labels.scaleBar });
|
|
3875
|
+
if (layout.elements.some((e) => e.kind === "extension")) {
|
|
3876
|
+
entries.push({ swatch: alignedTipSwatch, label: labels.alignedTip });
|
|
3877
|
+
}
|
|
3878
|
+
const block = legendBlock(entries, layout.height);
|
|
3879
|
+
if (block.svg !== "") {
|
|
3880
|
+
parts.push(block.svg);
|
|
3881
|
+
width = Math.max(width, block.width);
|
|
3882
|
+
height = block.height;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
const w = Math.ceil(width);
|
|
3886
|
+
const h = Math.ceil(height);
|
|
3887
|
+
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[layout.mode])}">` + parts.join("") + `</svg>`;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
// src/phylo/render.ts
|
|
3891
|
+
function phyloSvg(input, opts = {}) {
|
|
3892
|
+
const layout = computePhyloLayout(input, {
|
|
3893
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {},
|
|
3894
|
+
...opts.alignTips !== void 0 ? { alignTips: opts.alignTips } : {},
|
|
3895
|
+
...opts.showSupport !== void 0 ? { showSupport: opts.showSupport } : {},
|
|
3896
|
+
...opts.scaleBar !== void 0 ? { scaleBar: opts.scaleBar } : {},
|
|
3897
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
3898
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
3899
|
+
});
|
|
3900
|
+
const svg = phyloLayoutSvg(layout, {
|
|
3901
|
+
...opts.legend === false ? { legend: false } : {},
|
|
3902
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
3903
|
+
});
|
|
3904
|
+
return { svg, layout };
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
// src/org-chart/types.ts
|
|
3908
|
+
var ORG_REPORT_KINDS = ["line", "assistant", "dotted"];
|
|
3909
|
+
var ORG_VACANCIES = ["filled", "vacant"];
|
|
3910
|
+
|
|
3911
|
+
// src/org-chart/labels.ts
|
|
3912
|
+
var ORG_CHART_TITLE_LABELS_EN = {
|
|
3913
|
+
reportKinds: { line: "Reports to", assistant: "Assistant to", dotted: "Dotted-line report to" },
|
|
3914
|
+
vacant: "(vacant)"
|
|
3915
|
+
};
|
|
3916
|
+
var ORG_CHART_SVG_LABELS_EN = {
|
|
3917
|
+
legend: { line: "Reports to", assistant: "Assistant", dotted: "Dotted-line / matrix", vacant: "Vacant position" },
|
|
3918
|
+
ariaLabel: "Organizational chart"
|
|
3919
|
+
};
|
|
3920
|
+
|
|
3921
|
+
// src/org-chart/validate.ts
|
|
3922
|
+
var ORG_MAX_MATRIX_EDGES_PER_NODE = 5;
|
|
3923
|
+
var OrgChartValidationError = class extends Error {
|
|
3924
|
+
issues;
|
|
3925
|
+
constructor(issues) {
|
|
3926
|
+
super(`invalid org chart: ${issues.map((i) => i.message).join("; ")}`);
|
|
3927
|
+
this.name = "OrgChartValidationError";
|
|
3928
|
+
this.issues = issues;
|
|
3929
|
+
}
|
|
3930
|
+
};
|
|
3931
|
+
function listIds2(ids) {
|
|
3932
|
+
if (ids.length === 1) return String(ids[0]);
|
|
3933
|
+
return `${ids.slice(0, -1).join(", ")} and ${ids[ids.length - 1]}`;
|
|
3934
|
+
}
|
|
3935
|
+
function sortIssues4(issues) {
|
|
3936
|
+
const unique = /* @__PURE__ */ new Map();
|
|
3937
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
3938
|
+
return [...unique.values()].sort(
|
|
3939
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
3940
|
+
);
|
|
3941
|
+
}
|
|
3942
|
+
var GRAPH_BLOCKING = /* @__PURE__ */ new Set([
|
|
3943
|
+
"duplicate-id",
|
|
3944
|
+
"unknown-manager",
|
|
3945
|
+
"unknown-report",
|
|
3946
|
+
"multiple-managers"
|
|
3947
|
+
]);
|
|
3948
|
+
function orgChartIssues(input) {
|
|
3949
|
+
if (input.positions.length === 0 && input.reports.length === 0) return [];
|
|
3950
|
+
const issues = [];
|
|
3951
|
+
const push = (code, message) => {
|
|
3952
|
+
issues.push({ code, message });
|
|
3953
|
+
};
|
|
3954
|
+
const positionById = /* @__PURE__ */ new Map();
|
|
3955
|
+
const dupPositionIds = /* @__PURE__ */ new Set();
|
|
3956
|
+
for (const p of input.positions) {
|
|
3957
|
+
if (positionById.has(p.id)) dupPositionIds.add(p.id);
|
|
3958
|
+
else positionById.set(p.id, p);
|
|
3959
|
+
}
|
|
3960
|
+
for (const id of [...dupPositionIds].sort((a, b) => a - b)) {
|
|
3961
|
+
push("duplicate-id", `duplicate position id ${id}`);
|
|
3962
|
+
}
|
|
3963
|
+
const reportById = /* @__PURE__ */ new Map();
|
|
3964
|
+
const dupReportIds = /* @__PURE__ */ new Set();
|
|
3965
|
+
for (const r of input.reports) {
|
|
3966
|
+
if (reportById.has(r.id)) dupReportIds.add(r.id);
|
|
3967
|
+
else reportById.set(r.id, r);
|
|
3968
|
+
}
|
|
3969
|
+
for (const id of [...dupReportIds].sort((a, b) => a - b)) {
|
|
3970
|
+
push("duplicate-id", `duplicate report id ${id}`);
|
|
3971
|
+
}
|
|
3972
|
+
if (issues.length > 0) {
|
|
3973
|
+
return sortIssues4(issues);
|
|
3974
|
+
}
|
|
3975
|
+
const reports = [...reportById.values()].sort((a, b) => a.id - b.id);
|
|
3976
|
+
const solidParents = /* @__PURE__ */ new Map();
|
|
3977
|
+
const assistantReportEdge = /* @__PURE__ */ new Map();
|
|
3978
|
+
const solidManagedEdges = /* @__PURE__ */ new Map();
|
|
3979
|
+
const dottedDegree = /* @__PURE__ */ new Map();
|
|
3980
|
+
for (const r of reports) {
|
|
3981
|
+
const hasManager = positionById.has(r.managerId);
|
|
3982
|
+
const hasReport = positionById.has(r.reportId);
|
|
3983
|
+
if (!hasManager) {
|
|
3984
|
+
push("unknown-manager", `report ${r.id} managerId ${r.managerId} is not a declared position`);
|
|
3985
|
+
}
|
|
3986
|
+
if (!hasReport) {
|
|
3987
|
+
push("unknown-report", `report ${r.id} reportId ${r.reportId} is not a declared position`);
|
|
3988
|
+
}
|
|
3989
|
+
if (r.managerId === r.reportId) {
|
|
3990
|
+
push("self-report", `report ${r.id} has managerId === reportId (${r.managerId})`);
|
|
3991
|
+
}
|
|
3992
|
+
if (r.kind !== "dotted") {
|
|
3993
|
+
const arr = solidParents.get(r.reportId) ?? [];
|
|
3994
|
+
arr.push(r.id);
|
|
3995
|
+
solidParents.set(r.reportId, arr);
|
|
3996
|
+
const managed = solidManagedEdges.get(r.managerId) ?? [];
|
|
3997
|
+
managed.push(r.id);
|
|
3998
|
+
solidManagedEdges.set(r.managerId, managed);
|
|
3999
|
+
}
|
|
4000
|
+
if (r.kind === "assistant" && r.managerId !== r.reportId && !assistantReportEdge.has(r.reportId)) {
|
|
4001
|
+
assistantReportEdge.set(r.reportId, r.id);
|
|
4002
|
+
}
|
|
4003
|
+
if (r.kind === "dotted" && r.managerId !== r.reportId) {
|
|
4004
|
+
if (positionById.has(r.managerId)) dottedDegree.set(r.managerId, (dottedDegree.get(r.managerId) ?? 0) + 1);
|
|
4005
|
+
if (positionById.has(r.reportId)) dottedDegree.set(r.reportId, (dottedDegree.get(r.reportId) ?? 0) + 1);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
for (const [reportId, edgeIds] of [...solidParents.entries()].sort((a, b) => a[0] - b[0])) {
|
|
4009
|
+
if (edgeIds.length > 1) {
|
|
4010
|
+
push(
|
|
4011
|
+
"multiple-managers",
|
|
4012
|
+
`position ${reportId} is the report of solid edges ${listIds2(edgeIds.sort((a, b) => a - b))} \u2014 keep one solid line, make the rest kind "dotted"`
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
for (const [positionId, assistEdgeId] of [...assistantReportEdge.entries()].sort((a, b) => a[0] - b[0])) {
|
|
4017
|
+
const managed = solidManagedEdges.get(positionId);
|
|
4018
|
+
if (managed !== void 0 && managed.length > 0) {
|
|
4019
|
+
const firstManaged = Math.min(...managed);
|
|
4020
|
+
push(
|
|
4021
|
+
"assistant-has-reports",
|
|
4022
|
+
`position ${positionId} is an assistant (report of edge ${assistEdgeId}) but manages solid edge ${firstManaged} \u2014 assistants are leaves`
|
|
4023
|
+
);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
for (const [positionId, count] of [...dottedDegree.entries()].sort((a, b) => a[0] - b[0])) {
|
|
4027
|
+
if (count > ORG_MAX_MATRIX_EDGES_PER_NODE) {
|
|
4028
|
+
push(
|
|
4029
|
+
"too-many-matrix-edges",
|
|
4030
|
+
`position ${positionId} has ${count} dotted (matrix) connections \u2014 at most ${ORG_MAX_MATRIX_EDGES_PER_NODE} are supported per position`
|
|
4031
|
+
);
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
4035
|
+
const solidChildren = /* @__PURE__ */ new Map();
|
|
4036
|
+
for (const r of reports) {
|
|
4037
|
+
if (r.kind === "dotted") continue;
|
|
4038
|
+
if (r.managerId === r.reportId) continue;
|
|
4039
|
+
const arr = solidChildren.get(r.managerId) ?? [];
|
|
4040
|
+
arr.push(r.reportId);
|
|
4041
|
+
solidChildren.set(r.managerId, arr);
|
|
4042
|
+
}
|
|
4043
|
+
for (const arr of solidChildren.values()) arr.sort((a, b) => a - b);
|
|
4044
|
+
const color = /* @__PURE__ */ new Map();
|
|
4045
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
4046
|
+
const dfs = (start) => {
|
|
4047
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
4048
|
+
color.set(start, 1);
|
|
4049
|
+
while (stack.length > 0) {
|
|
4050
|
+
const frame = stack[stack.length - 1];
|
|
4051
|
+
const children = solidChildren.get(frame.id) ?? [];
|
|
4052
|
+
if (frame.nextChild >= children.length) {
|
|
4053
|
+
color.set(frame.id, 2);
|
|
4054
|
+
stack.pop();
|
|
4055
|
+
continue;
|
|
4056
|
+
}
|
|
4057
|
+
const child = children[frame.nextChild];
|
|
4058
|
+
frame.nextChild += 1;
|
|
4059
|
+
const c = color.get(child) ?? 0;
|
|
4060
|
+
if (c === 1) {
|
|
4061
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
4062
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
4063
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
4064
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
4065
|
+
const key = rotated.join(">");
|
|
4066
|
+
if (!seenCycles.has(key)) {
|
|
4067
|
+
seenCycles.add(key);
|
|
4068
|
+
push("cycle", `cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`);
|
|
4069
|
+
}
|
|
4070
|
+
} else if (c === 0) {
|
|
4071
|
+
color.set(child, 1);
|
|
4072
|
+
stack.push({ id: child, nextChild: 0 });
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
for (const p of [...positionById.values()].sort((a, b) => a.id - b.id)) {
|
|
4077
|
+
if ((color.get(p.id) ?? 0) === 0) dfs(p.id);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
return sortIssues4(issues);
|
|
4081
|
+
}
|
|
4082
|
+
function validateOrgChart(input) {
|
|
4083
|
+
const issues = orgChartIssues(input);
|
|
4084
|
+
if (issues.length > 0) throw new OrgChartValidationError(issues);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// src/org-chart/layout.ts
|
|
4088
|
+
var ORG_LABEL_FONT = 12;
|
|
4089
|
+
var ORG_TITLE_FONT = 10;
|
|
4090
|
+
var ORG_LABEL_LINE_H = 14;
|
|
4091
|
+
var PADDING7 = 32;
|
|
4092
|
+
var ORG_SIBLING_GAP = 28;
|
|
4093
|
+
var ORG_TREE_GAP = 56;
|
|
4094
|
+
var ORG_CORRIDOR = 30;
|
|
4095
|
+
var ORG_MIN_BOX_W = 120;
|
|
4096
|
+
var ORG_BOX_PAD_X = 12;
|
|
4097
|
+
var ORG_BOX_PAD_Y = 9;
|
|
4098
|
+
var ORG_ASSIST_GAP = 24;
|
|
4099
|
+
var ORG_ASSIST_BAND_DROP = 14;
|
|
4100
|
+
var ORG_MATRIX_GUTTER_GAP = 28;
|
|
4101
|
+
var ORG_MATRIX_LANE_PITCH = 14;
|
|
4102
|
+
var ORG_MATRIX_ESCAPE_STEP = 2;
|
|
4103
|
+
var ORG_STEM_ID_BASE = 1e6;
|
|
4104
|
+
var ORG_BUS_ID_BASE = 2e6;
|
|
4105
|
+
var ORG_DROP_ID_BASE = 3e6;
|
|
4106
|
+
var ORG_ASSIST_ID_BASE = 4e6;
|
|
4107
|
+
var ORG_DOTTED_ID_BASE = 5e6;
|
|
4108
|
+
var round9 = (n) => Math.round(n * 100) / 100;
|
|
4109
|
+
var drawnLeftEdge = (cx, boxW) => round9(round9(cx) - round9(boxW) / 2);
|
|
4110
|
+
var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) + round9(boxW);
|
|
4111
|
+
function packSubtree(node, gaps) {
|
|
4112
|
+
const { ownHalfL, ownHalfR, children } = node;
|
|
4113
|
+
if (children.length === 0) {
|
|
4114
|
+
return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
|
|
4115
|
+
}
|
|
4116
|
+
const xs = [0];
|
|
4117
|
+
for (let i = 1; i < children.length; i++) {
|
|
4118
|
+
xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
|
|
4119
|
+
}
|
|
4120
|
+
const first = children[0];
|
|
4121
|
+
const last = children[children.length - 1];
|
|
4122
|
+
const axis = (xs[0] + xs[xs.length - 1]) / 2;
|
|
4123
|
+
const offsets = xs.map((x) => x - axis);
|
|
4124
|
+
const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
|
|
4125
|
+
const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
|
|
4126
|
+
return { halfL, halfR, offsets };
|
|
4127
|
+
}
|
|
4128
|
+
function measurePosition(position, maxLabelChars, vacantWord) {
|
|
4129
|
+
const nameLines = position.name === "" ? [] : wrapLabelBalanced(clampLabel(position.name, maxLabelChars), 2);
|
|
4130
|
+
const titleLines = position.title === null ? [] : wrapLabelBalanced(clampLabel(position.title, maxLabelChars), 2);
|
|
4131
|
+
const subtitleLines = position.subtitle === null ? [] : wrapLabelBalanced(clampLabel(position.subtitle, maxLabelChars), 1);
|
|
4132
|
+
const vacantMarker = position.vacancy === "vacant" ? vacantWord : null;
|
|
4133
|
+
let innerW = 0;
|
|
4134
|
+
for (const l of nameLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_LABEL_FONT));
|
|
4135
|
+
for (const l of titleLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_TITLE_FONT));
|
|
4136
|
+
for (const l of subtitleLines) innerW = Math.max(innerW, estimateTextWidth(l, ORG_TITLE_FONT));
|
|
4137
|
+
if (vacantMarker !== null) innerW = Math.max(innerW, estimateTextWidth(vacantMarker, ORG_TITLE_FONT));
|
|
4138
|
+
const boxW = Math.max(ORG_MIN_BOX_W, innerW + ORG_BOX_PAD_X * 2);
|
|
4139
|
+
const lineCount = nameLines.length + titleLines.length + subtitleLines.length + (vacantMarker !== null ? 1 : 0);
|
|
4140
|
+
const boxH = ORG_BOX_PAD_Y * 2 + lineCount * ORG_LABEL_LINE_H;
|
|
4141
|
+
return { boxW, boxH, nameLines, titleLines, subtitleLines, vacantMarker };
|
|
4142
|
+
}
|
|
4143
|
+
function positionTitle(position, vacantWord) {
|
|
4144
|
+
if (position.hover !== void 0) return position.hover;
|
|
4145
|
+
if (position.title !== null) {
|
|
4146
|
+
return position.name === "" ? position.title : `${position.name} \u2014 ${position.title}`;
|
|
4147
|
+
}
|
|
4148
|
+
if (position.name !== "") return position.name;
|
|
4149
|
+
return vacantWord;
|
|
4150
|
+
}
|
|
4151
|
+
function computeOrgChartLayout(input, opts = {}) {
|
|
4152
|
+
if (input.positions.length === 0 && input.reports.length === 0) {
|
|
4153
|
+
return { width: PADDING7 * 2, height: PADDING7 * 2, nodes: [], elements: [], rootPositionIds: [] };
|
|
4154
|
+
}
|
|
4155
|
+
validateOrgChart(input);
|
|
4156
|
+
const titleLabels = opts.titleLabels ?? ORG_CHART_TITLE_LABELS_EN;
|
|
4157
|
+
const vacantWord = titleLabels.vacant;
|
|
4158
|
+
const positionById = new Map(input.positions.map((p) => [p.id, p]));
|
|
4159
|
+
const reports = [...input.reports].sort((a, b) => a.id - b.id);
|
|
4160
|
+
const solidReports = reports.filter((r) => r.kind !== "dotted");
|
|
4161
|
+
const lineChildIds = /* @__PURE__ */ new Map();
|
|
4162
|
+
const assistChildIds = /* @__PURE__ */ new Map();
|
|
4163
|
+
const solidParentReportId = /* @__PURE__ */ new Set();
|
|
4164
|
+
for (const r of solidReports) {
|
|
4165
|
+
solidParentReportId.add(r.reportId);
|
|
4166
|
+
const bucket = r.kind === "assistant" ? assistChildIds : lineChildIds;
|
|
4167
|
+
const arr = bucket.get(r.managerId) ?? [];
|
|
4168
|
+
arr.push(r);
|
|
4169
|
+
bucket.set(r.managerId, arr);
|
|
4170
|
+
}
|
|
4171
|
+
for (const arr of lineChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
4172
|
+
for (const arr of assistChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
|
|
4173
|
+
const rootPositionIds = input.positions.map((p) => p.id).filter((id) => !solidParentReportId.has(id)).sort((a, b) => a - b);
|
|
4174
|
+
const newInst = (position, depth, assistReport) => {
|
|
4175
|
+
const m = measurePosition(position, opts.maxLabelChars, vacantWord);
|
|
4176
|
+
return {
|
|
4177
|
+
position,
|
|
4178
|
+
m,
|
|
4179
|
+
style: position.vacancy === "vacant" ? "dashed" : "solid",
|
|
4180
|
+
title: positionTitle(position, vacantWord),
|
|
4181
|
+
depth,
|
|
4182
|
+
lineChildren: [],
|
|
4183
|
+
assistants: [],
|
|
4184
|
+
ownHalfL: m.boxW / 2,
|
|
4185
|
+
ownHalfR: m.boxW / 2,
|
|
4186
|
+
spanL: 0,
|
|
4187
|
+
spanR: 0,
|
|
4188
|
+
offsets: [],
|
|
4189
|
+
cx: 0,
|
|
4190
|
+
assistReport,
|
|
4191
|
+
assistTop: 0
|
|
4192
|
+
};
|
|
4193
|
+
};
|
|
4194
|
+
const allInsts = [];
|
|
4195
|
+
const build = (positionId, depth, assistReport) => {
|
|
4196
|
+
const position = positionById.get(positionId);
|
|
4197
|
+
const inst = newInst(position, depth, assistReport);
|
|
4198
|
+
allInsts.push(inst);
|
|
4199
|
+
for (const r of assistChildIds.get(positionId) ?? []) {
|
|
4200
|
+
const a = build(r.reportId, depth + 1, r);
|
|
4201
|
+
inst.assistants.push({ inst: a, report: r });
|
|
4202
|
+
}
|
|
4203
|
+
for (const r of lineChildIds.get(positionId) ?? []) {
|
|
4204
|
+
inst.lineChildren.push(build(r.reportId, depth + 1, null));
|
|
4205
|
+
}
|
|
4206
|
+
return inst;
|
|
4207
|
+
};
|
|
4208
|
+
const roots = rootPositionIds.map((id) => build(id, 0, null));
|
|
4209
|
+
for (const inst of allInsts) {
|
|
4210
|
+
if (inst.assistants.length > 0) {
|
|
4211
|
+
const widest = inst.assistants.reduce((m, a) => Math.max(m, a.inst.m.boxW), 0);
|
|
4212
|
+
inst.ownHalfL = inst.m.boxW / 2 + ORG_ASSIST_GAP + widest;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
const gaps = { siblingGap: ORG_SIBLING_GAP };
|
|
4216
|
+
const pack = (inst) => {
|
|
4217
|
+
for (const c of inst.lineChildren) pack(c);
|
|
4218
|
+
const result = packSubtree(
|
|
4219
|
+
{
|
|
4220
|
+
ownHalfL: inst.ownHalfL,
|
|
4221
|
+
ownHalfR: inst.ownHalfR,
|
|
4222
|
+
children: inst.lineChildren.map((c) => ({ halfL: c.spanL, halfR: c.spanR }))
|
|
4223
|
+
},
|
|
4224
|
+
gaps
|
|
4225
|
+
);
|
|
4226
|
+
inst.spanL = result.halfL;
|
|
4227
|
+
inst.spanR = result.halfR;
|
|
4228
|
+
inst.offsets = result.offsets;
|
|
4229
|
+
};
|
|
4230
|
+
for (const root of roots) pack(root);
|
|
4231
|
+
let cursor = PADDING7;
|
|
4232
|
+
for (const root of roots) {
|
|
4233
|
+
root.cx = cursor + root.spanL;
|
|
4234
|
+
const placeX = (inst) => {
|
|
4235
|
+
inst.lineChildren.forEach((c, i) => {
|
|
4236
|
+
c.cx = inst.cx + inst.offsets[i];
|
|
4237
|
+
placeX(c);
|
|
4238
|
+
});
|
|
4239
|
+
};
|
|
4240
|
+
placeX(root);
|
|
4241
|
+
cursor = root.cx + root.spanR + ORG_TREE_GAP;
|
|
4242
|
+
}
|
|
4243
|
+
const canvasRight = roots.length === 0 ? PADDING7 : cursor - ORG_TREE_GAP;
|
|
4244
|
+
const rowInsts = [];
|
|
4245
|
+
for (const inst of allInsts) (rowInsts[inst.depth] ??= []).push(inst);
|
|
4246
|
+
const depthCount = rowInsts.length;
|
|
4247
|
+
const assistStackH = (inst) => {
|
|
4248
|
+
if (inst.assistants.length === 0) return 0;
|
|
4249
|
+
return inst.assistants.reduce((s, a) => s + a.inst.m.boxH + ORG_ASSIST_BAND_DROP, 0) + ORG_ASSIST_BAND_DROP;
|
|
4250
|
+
};
|
|
4251
|
+
const rowH = [];
|
|
4252
|
+
for (let d = 0; d < depthCount; d++) {
|
|
4253
|
+
const lineBoxes = (rowInsts[d] ?? []).filter((i) => i.assistReport === null);
|
|
4254
|
+
rowH.push(lineBoxes.reduce((m, i) => Math.max(m, i.m.boxH), 0));
|
|
4255
|
+
}
|
|
4256
|
+
const assistZone = [];
|
|
4257
|
+
for (let d = 0; d < depthCount; d++) {
|
|
4258
|
+
let tallest = 0;
|
|
4259
|
+
for (const inst of rowInsts[d] ?? []) tallest = Math.max(tallest, assistStackH(inst));
|
|
4260
|
+
assistZone.push(tallest);
|
|
4261
|
+
}
|
|
4262
|
+
const rowTop = [PADDING7];
|
|
4263
|
+
for (let d = 0; d < depthCount - 1; d++) {
|
|
4264
|
+
rowTop.push(rowTop[d] + rowH[d] + assistZone[d] + ORG_CORRIDOR);
|
|
4265
|
+
}
|
|
4266
|
+
const busY = (d) => rowTop[d + 1] - ORG_CORRIDOR / 2;
|
|
4267
|
+
const last = depthCount - 1;
|
|
4268
|
+
const height = Math.ceil(rowTop[last] + rowH[last] + assistZone[last] + PADDING7);
|
|
4269
|
+
const nodes = [];
|
|
4270
|
+
const elements = [];
|
|
4271
|
+
const reportTitle = (kind, label) => {
|
|
4272
|
+
const word = titleLabels.reportKinds[kind];
|
|
4273
|
+
return label !== null && label !== void 0 ? `${word} \xB7 ${label}` : word;
|
|
4274
|
+
};
|
|
4275
|
+
const pushNode = (inst) => {
|
|
4276
|
+
nodes.push({
|
|
4277
|
+
positionId: inst.position.id,
|
|
4278
|
+
cx: round9(inst.cx),
|
|
4279
|
+
top: round9(rowTop[inst.depth]),
|
|
4280
|
+
boxW: round9(inst.m.boxW),
|
|
4281
|
+
boxH: round9(inst.m.boxH),
|
|
4282
|
+
style: inst.style,
|
|
4283
|
+
nameLines: inst.m.nameLines,
|
|
4284
|
+
titleLines: inst.m.titleLines,
|
|
4285
|
+
subtitleLines: inst.m.subtitleLines,
|
|
4286
|
+
vacantMarker: inst.m.vacantMarker,
|
|
4287
|
+
isAssistant: inst.assistReport !== null,
|
|
4288
|
+
depth: inst.depth,
|
|
4289
|
+
title: inst.title
|
|
4290
|
+
});
|
|
4291
|
+
};
|
|
4292
|
+
const emit = (inst) => {
|
|
4293
|
+
const d = inst.depth;
|
|
4294
|
+
let bandY = rowTop[d] + rowH[d] + ORG_ASSIST_BAND_DROP;
|
|
4295
|
+
for (const { inst: a, report } of inst.assistants) {
|
|
4296
|
+
const assistRight = inst.cx - ORG_ASSIST_GAP;
|
|
4297
|
+
a.cx = assistRight - a.m.boxW / 2;
|
|
4298
|
+
const assistTop = bandY;
|
|
4299
|
+
a.assistTop = assistTop;
|
|
4300
|
+
const assistMidY = assistTop + a.m.boxH / 2;
|
|
4301
|
+
nodes.push({
|
|
4302
|
+
positionId: a.position.id,
|
|
4303
|
+
cx: round9(a.cx),
|
|
4304
|
+
top: round9(assistTop),
|
|
4305
|
+
boxW: round9(a.m.boxW),
|
|
4306
|
+
boxH: round9(a.m.boxH),
|
|
4307
|
+
style: a.style,
|
|
4308
|
+
nameLines: a.m.nameLines,
|
|
4309
|
+
titleLines: a.m.titleLines,
|
|
4310
|
+
subtitleLines: a.m.subtitleLines,
|
|
4311
|
+
vacantMarker: a.m.vacantMarker,
|
|
4312
|
+
isAssistant: true,
|
|
4313
|
+
depth: a.depth,
|
|
4314
|
+
title: a.title
|
|
4315
|
+
});
|
|
4316
|
+
elements.push({
|
|
4317
|
+
edgeId: ORG_ASSIST_ID_BASE + report.reportId,
|
|
4318
|
+
kind: "assist",
|
|
4319
|
+
points: [
|
|
4320
|
+
{ x: round9(inst.cx), y: round9(assistMidY) },
|
|
4321
|
+
{ x: drawnRightEdge(a.cx, a.m.boxW), y: round9(assistMidY) }
|
|
4322
|
+
],
|
|
4323
|
+
dashed: false,
|
|
4324
|
+
title: reportTitle("assistant", report.label)
|
|
4325
|
+
});
|
|
4326
|
+
bandY += a.m.boxH + ORG_ASSIST_BAND_DROP;
|
|
4327
|
+
}
|
|
4328
|
+
pushNode(inst);
|
|
4329
|
+
if (inst.lineChildren.length === 0) return;
|
|
4330
|
+
const by = busY(d);
|
|
4331
|
+
const boxBottom = rowTop[d] + inst.m.boxH;
|
|
4332
|
+
elements.push({
|
|
4333
|
+
edgeId: ORG_STEM_ID_BASE + inst.position.id,
|
|
4334
|
+
kind: "stem",
|
|
4335
|
+
points: [
|
|
4336
|
+
{ x: round9(inst.cx), y: round9(boxBottom) },
|
|
4337
|
+
{ x: round9(inst.cx), y: round9(by) }
|
|
4338
|
+
],
|
|
4339
|
+
dashed: false,
|
|
4340
|
+
title: reportTitle("line", null)
|
|
4341
|
+
});
|
|
4342
|
+
const childCxs = inst.lineChildren.map((c) => c.cx);
|
|
4343
|
+
if (inst.lineChildren.length > 1) {
|
|
4344
|
+
elements.push({
|
|
4345
|
+
edgeId: ORG_BUS_ID_BASE + inst.position.id,
|
|
4346
|
+
kind: "bus",
|
|
4347
|
+
points: [
|
|
4348
|
+
{ x: round9(Math.min(...childCxs)), y: round9(by) },
|
|
4349
|
+
{ x: round9(Math.max(...childCxs)), y: round9(by) }
|
|
4350
|
+
],
|
|
4351
|
+
dashed: false,
|
|
4352
|
+
title: reportTitle("line", null)
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
for (const c of inst.lineChildren) {
|
|
4356
|
+
elements.push({
|
|
4357
|
+
edgeId: ORG_DROP_ID_BASE + c.position.id,
|
|
4358
|
+
kind: "drop",
|
|
4359
|
+
points: [
|
|
4360
|
+
{ x: round9(c.cx), y: round9(by) },
|
|
4361
|
+
{ x: round9(c.cx), y: round9(rowTop[c.depth]) }
|
|
4362
|
+
],
|
|
4363
|
+
dashed: false,
|
|
4364
|
+
title: reportTitle("line", null)
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
for (const c of inst.lineChildren) emit(c);
|
|
4368
|
+
};
|
|
4369
|
+
for (const root of roots) emit(root);
|
|
4370
|
+
const instByPos = new Map(allInsts.map((i) => [i.position.id, i]));
|
|
4371
|
+
const escapeByPos = /* @__PURE__ */ new Map();
|
|
4372
|
+
for (const inst of allInsts) {
|
|
4373
|
+
const isAssist = inst.assistReport !== null;
|
|
4374
|
+
const boxTop = isAssist ? inst.assistTop : rowTop[inst.depth];
|
|
4375
|
+
const leftEdge = drawnLeftEdge(inst.cx, inst.m.boxW);
|
|
4376
|
+
const rightEdge = drawnRightEdge(inst.cx, inst.m.boxW);
|
|
4377
|
+
if (isAssist) {
|
|
4378
|
+
const managerInst = instByPos.get(inst.assistReport.managerId);
|
|
4379
|
+
escapeByPos.set(inst.position.id, {
|
|
4380
|
+
side: -1,
|
|
4381
|
+
boxEdge: leftEdge,
|
|
4382
|
+
escCol: managerInst.cx - managerInst.spanL,
|
|
4383
|
+
midY: boxTop + inst.m.boxH / 2,
|
|
4384
|
+
boxH: inst.m.boxH
|
|
4385
|
+
});
|
|
4386
|
+
} else {
|
|
4387
|
+
escapeByPos.set(inst.position.id, {
|
|
4388
|
+
side: 1,
|
|
4389
|
+
boxEdge: rightEdge,
|
|
4390
|
+
escCol: inst.cx + inst.spanR,
|
|
4391
|
+
midY: boxTop + inst.m.boxH / 2,
|
|
4392
|
+
boxH: inst.m.boxH
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
const nodeById = new Map(nodes.map((n) => [n.positionId, n]));
|
|
4397
|
+
const dottedReports = reports.filter(
|
|
4398
|
+
(r) => r.kind === "dotted" && nodeById.has(r.managerId) && nodeById.has(r.reportId) && r.managerId !== r.reportId
|
|
4399
|
+
);
|
|
4400
|
+
let maxBoxBottom = PADDING7;
|
|
4401
|
+
for (const n of nodes) maxBoxBottom = Math.max(maxBoxBottom, n.top + n.boxH);
|
|
4402
|
+
let laneCount = 0;
|
|
4403
|
+
let channelLanes = 0;
|
|
4404
|
+
if (dottedReports.length > 0) {
|
|
4405
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
4406
|
+
laneCount = allocateLanes2(
|
|
4407
|
+
dottedReports.map((r) => {
|
|
4408
|
+
const rep = escapeByPos.get(r.reportId);
|
|
4409
|
+
const mgr = escapeByPos.get(r.managerId);
|
|
4410
|
+
return {
|
|
4411
|
+
lo: Math.min(rep.midY, mgr.midY),
|
|
4412
|
+
hi: Math.max(rep.midY, mgr.midY),
|
|
4413
|
+
set: (lane) => laneOf.set(r.id, lane)
|
|
4414
|
+
};
|
|
4415
|
+
})
|
|
4416
|
+
);
|
|
4417
|
+
channelLanes = dottedReports.length * 2;
|
|
4418
|
+
const channelTop = maxBoxBottom + ORG_MATRIX_GUTTER_GAP;
|
|
4419
|
+
const exitSlot = /* @__PURE__ */ new Map();
|
|
4420
|
+
const nextExitY = (posId, mid, boxH) => {
|
|
4421
|
+
const slot = exitSlot.get(posId) ?? 0;
|
|
4422
|
+
exitSlot.set(posId, slot + 1);
|
|
4423
|
+
if (slot === 0) return mid;
|
|
4424
|
+
const step = Math.min(6, Math.max(2, (boxH / 2 - 6) / Math.max(1, Math.ceil(slot / 2))));
|
|
4425
|
+
const sign = slot % 2 === 1 ? -1 : 1;
|
|
4426
|
+
return mid + sign * Math.ceil(slot / 2) * step;
|
|
4427
|
+
};
|
|
4428
|
+
const geoms = dottedReports.map((r, i) => {
|
|
4429
|
+
const rep = escapeByPos.get(r.reportId);
|
|
4430
|
+
const mgr = escapeByPos.get(r.managerId);
|
|
4431
|
+
return {
|
|
4432
|
+
r,
|
|
4433
|
+
i,
|
|
4434
|
+
repMidY: round9(nextExitY(r.reportId, rep.midY, rep.boxH)),
|
|
4435
|
+
mgrMidY: round9(nextExitY(r.managerId, mgr.midY, mgr.boxH)),
|
|
4436
|
+
repBoxEdge: round9(rep.boxEdge),
|
|
4437
|
+
mgrBoxEdge: round9(mgr.boxEdge),
|
|
4438
|
+
repChY: round9(channelTop + 2 * i * ORG_MATRIX_LANE_PITCH),
|
|
4439
|
+
mgrChY: round9(channelTop + (2 * i + 1) * ORG_MATRIX_LANE_PITCH),
|
|
4440
|
+
laneX: round9(canvasRight + ORG_MATRIX_GUTTER_GAP + laneOf.get(r.id) * ORG_MATRIX_LANE_PITCH),
|
|
4441
|
+
repEscX: 0,
|
|
4442
|
+
mgrEscX: 0
|
|
4443
|
+
};
|
|
4444
|
+
});
|
|
4445
|
+
const verts = [];
|
|
4446
|
+
for (const g of geoms) {
|
|
4447
|
+
const rep = escapeByPos.get(g.r.reportId);
|
|
4448
|
+
const mgr = escapeByPos.get(g.r.managerId);
|
|
4449
|
+
verts.push({
|
|
4450
|
+
escCol: rep.escCol,
|
|
4451
|
+
side: rep.side,
|
|
4452
|
+
lo: Math.min(g.repMidY, g.repChY),
|
|
4453
|
+
hi: Math.max(g.repMidY, g.repChY),
|
|
4454
|
+
assign: (x) => g.repEscX = x
|
|
4455
|
+
});
|
|
4456
|
+
verts.push({
|
|
4457
|
+
escCol: mgr.escCol,
|
|
4458
|
+
side: mgr.side,
|
|
4459
|
+
lo: Math.min(g.mgrMidY, g.mgrChY),
|
|
4460
|
+
hi: Math.max(g.mgrMidY, g.mgrChY),
|
|
4461
|
+
assign: (x) => g.mgrEscX = x
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4464
|
+
const VERT_EPS = 0.01;
|
|
4465
|
+
const ordered = verts.map((v, idx) => ({ v, key: geoms[Math.floor(idx / 2)].r.id * 2 + idx % 2 })).sort((a, b) => a.key - b.key).map((e) => e.v);
|
|
4466
|
+
const placed = [];
|
|
4467
|
+
const yOverlap = (aLo, aHi, bLo, bHi) => Math.min(aHi, bHi) - Math.max(aLo, bLo) > VERT_EPS;
|
|
4468
|
+
for (const v of ordered) {
|
|
4469
|
+
let band = v.side === 1 ? Math.max(canvasRight - v.escCol, ORG_MATRIX_GUTTER_GAP) : v.escCol;
|
|
4470
|
+
for (const n of nodes) {
|
|
4471
|
+
const bLeft = n.cx - n.boxW / 2;
|
|
4472
|
+
const bRight = n.cx + n.boxW / 2;
|
|
4473
|
+
if (n.top + n.boxH <= v.lo + VERT_EPS || n.top >= v.hi - VERT_EPS) continue;
|
|
4474
|
+
if (v.side === 1 && bLeft > v.escCol + VERT_EPS) band = Math.min(band, bLeft - v.escCol);
|
|
4475
|
+
else if (v.side === -1 && bRight < v.escCol - VERT_EPS) band = Math.min(band, v.escCol - bRight);
|
|
4476
|
+
}
|
|
4477
|
+
let lane = 0;
|
|
4478
|
+
let candidate = round9(v.escCol);
|
|
4479
|
+
while (placed.some((p) => Math.abs(p.x - candidate) < VERT_EPS && yOverlap(p.lo, p.hi, v.lo, v.hi))) {
|
|
4480
|
+
lane += 1;
|
|
4481
|
+
candidate = round9(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
|
|
4482
|
+
}
|
|
4483
|
+
if (lane > 0 && lane * ORG_MATRIX_ESCAPE_STEP >= band - VERT_EPS) {
|
|
4484
|
+
throw new Error(
|
|
4485
|
+
`org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${round9(band)}px box-free band`
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
4488
|
+
placed.push({ x: candidate, lo: v.lo, hi: v.hi });
|
|
4489
|
+
v.assign(candidate);
|
|
4490
|
+
}
|
|
4491
|
+
for (const g of geoms) {
|
|
4492
|
+
elements.push({
|
|
4493
|
+
edgeId: ORG_DOTTED_ID_BASE + g.r.id,
|
|
4494
|
+
kind: "dotted",
|
|
4495
|
+
points: collapseDegenerate([
|
|
4496
|
+
{ x: g.repBoxEdge, y: g.repMidY },
|
|
4497
|
+
// out the report box's edge
|
|
4498
|
+
{ x: g.repEscX, y: g.repMidY },
|
|
4499
|
+
// → box-free escape column (in own gutter lane)
|
|
4500
|
+
{ x: g.repEscX, y: g.repChY },
|
|
4501
|
+
// → down into the bottom traverse channel
|
|
4502
|
+
{ x: g.laneX, y: g.repChY },
|
|
4503
|
+
// → across (below all boxes) to the right gutter lane
|
|
4504
|
+
{ x: g.laneX, y: g.mgrChY },
|
|
4505
|
+
// → down the right gutter (right of every box)
|
|
4506
|
+
{ x: g.mgrEscX, y: g.mgrChY },
|
|
4507
|
+
// → back across (below all boxes) to the manager column
|
|
4508
|
+
{ x: g.mgrEscX, y: g.mgrMidY },
|
|
4509
|
+
// → up the box-free escape column
|
|
4510
|
+
{ x: g.mgrBoxEdge, y: g.mgrMidY }
|
|
4511
|
+
// → into the manager box's edge
|
|
4512
|
+
]),
|
|
4513
|
+
dashed: true,
|
|
4514
|
+
title: reportTitle("dotted", g.r.label)
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
const width = dottedReports.length > 0 ? Math.ceil(canvasRight + ORG_MATRIX_GUTTER_GAP + (laneCount - 1) * ORG_MATRIX_LANE_PITCH + ORG_MATRIX_GUTTER_GAP + PADDING7) : Math.ceil(canvasRight + PADDING7);
|
|
4519
|
+
const finalHeight = channelLanes > 0 ? Math.ceil(maxBoxBottom + ORG_MATRIX_GUTTER_GAP + (channelLanes - 1) * ORG_MATRIX_LANE_PITCH + PADDING7) : height;
|
|
4520
|
+
return { width, height: finalHeight, nodes, elements, rootPositionIds };
|
|
4521
|
+
}
|
|
4522
|
+
function collapseDegenerate(points) {
|
|
4523
|
+
const out = [];
|
|
4524
|
+
for (const p of points) {
|
|
4525
|
+
const prev = out[out.length - 1];
|
|
4526
|
+
if (prev === void 0 || prev.x !== p.x || prev.y !== p.y) out.push(p);
|
|
4527
|
+
}
|
|
4528
|
+
return out;
|
|
4529
|
+
}
|
|
4530
|
+
function allocateLanes2(items) {
|
|
4531
|
+
const lanes = [];
|
|
4532
|
+
for (const it of items) {
|
|
4533
|
+
let chosen = -1;
|
|
4534
|
+
for (let l = 0; l < lanes.length; l++) {
|
|
4535
|
+
if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
|
|
4536
|
+
chosen = l;
|
|
4537
|
+
break;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
if (chosen === -1) {
|
|
4541
|
+
chosen = lanes.length;
|
|
4542
|
+
lanes.push([]);
|
|
4543
|
+
}
|
|
4544
|
+
lanes[chosen].push({ lo: it.lo, hi: it.hi });
|
|
4545
|
+
it.set(chosen);
|
|
4546
|
+
}
|
|
4547
|
+
return lanes.length;
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
// src/org-chart/svg.ts
|
|
4551
|
+
var GLYPH_STROKE5 = "#52525b";
|
|
4552
|
+
var LABEL_FILL6 = "#3f3f46";
|
|
4553
|
+
var EDGE_INK7 = "#71717a";
|
|
4554
|
+
var GLYPH_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="2"`;
|
|
4555
|
+
var VACANT_DASH = `stroke-dasharray="6,4"`;
|
|
4556
|
+
var DOTTED = EDGE_STROKE.distant;
|
|
4557
|
+
var round10 = (n) => Math.round(n * 100) / 100;
|
|
4558
|
+
var ORG_BOX_PAD_Y2 = 9;
|
|
4559
|
+
function boxSvg(n) {
|
|
4560
|
+
const left = round10(n.cx - n.boxW / 2);
|
|
4561
|
+
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
4562
|
+
if (n.style === "dashed") {
|
|
4563
|
+
pieces.push(
|
|
4564
|
+
`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`
|
|
4565
|
+
);
|
|
4566
|
+
const ix = round10(left + 3);
|
|
4567
|
+
const iy = round10(n.top + 3);
|
|
4568
|
+
const iw = round10(n.boxW - 6);
|
|
4569
|
+
const ih = round10(n.boxH - 6);
|
|
4570
|
+
pieces.push(`<rect x="${ix}" y="${iy}" width="${iw}" height="${ih}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`);
|
|
4571
|
+
} else {
|
|
4572
|
+
pieces.push(`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3}/>`);
|
|
4573
|
+
}
|
|
4574
|
+
let lineIndex = 0;
|
|
4575
|
+
const firstBaseline = (i) => round10(n.top + ORG_BOX_PAD_Y2 + ORG_LABEL_LINE_H / 2 + i * ORG_LABEL_LINE_H + ORG_LABEL_FONT * 0.32);
|
|
4576
|
+
const textBlock = (lines, font) => {
|
|
4577
|
+
if (lines.length === 0) return "";
|
|
4578
|
+
const tspans = lines.map((line, i) => `<tspan x="${n.cx}" y="${firstBaseline(lineIndex + i)}">${xmlEscape(line)}</tspan>`).join("");
|
|
4579
|
+
lineIndex += lines.length;
|
|
4580
|
+
return `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${font}" fill="${LABEL_FILL6}">${tspans}</text>`;
|
|
4581
|
+
};
|
|
4582
|
+
pieces.push(textBlock(n.nameLines, ORG_LABEL_FONT));
|
|
4583
|
+
pieces.push(textBlock(n.titleLines, ORG_TITLE_FONT));
|
|
4584
|
+
pieces.push(textBlock(n.subtitleLines, ORG_TITLE_FONT));
|
|
4585
|
+
if (n.vacantMarker !== null) pieces.push(textBlock([n.vacantMarker], ORG_TITLE_FONT));
|
|
4586
|
+
return `<g data-node-id="p${n.positionId}">${pieces.filter((p) => p !== "").join("")}</g>`;
|
|
4587
|
+
}
|
|
4588
|
+
function elementSvg5(el) {
|
|
4589
|
+
const head = `<g data-edge-id="${el.edgeId}"><title>${xmlEscape(el.title)}</title>`;
|
|
4590
|
+
if (el.dashed) {
|
|
4591
|
+
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
4592
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/></g>`;
|
|
4593
|
+
}
|
|
4594
|
+
if (el.points.length === 2) {
|
|
4595
|
+
const a = el.points[0];
|
|
4596
|
+
const b = el.points[1];
|
|
4597
|
+
return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
|
|
4598
|
+
}
|
|
4599
|
+
return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
|
|
4600
|
+
}
|
|
4601
|
+
var MINI_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="1.5"`;
|
|
4602
|
+
function lineSwatch(x, y) {
|
|
4603
|
+
const x1 = round10(x + 2);
|
|
4604
|
+
const x2 = round10(x + LEGEND_SWATCH_W - 2);
|
|
4605
|
+
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
4606
|
+
}
|
|
4607
|
+
function assistantSwatch(x, y) {
|
|
4608
|
+
const stemX = round10(x + LEGEND_SWATCH_W - 4);
|
|
4609
|
+
const boxX = round10(x + 1);
|
|
4610
|
+
return `<line x1="${stemX}" y1="${round10(y - 4)}" x2="${stemX}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><line x1="${stemX}" y1="${y}" x2="${round10(boxX + 8)}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><rect x="${boxX}" y="${round10(y - 3.5)}" width="8" height="7" rx="1" ${MINI_ATTRS3}/>`;
|
|
4611
|
+
}
|
|
4612
|
+
function dottedSwatch(x, y) {
|
|
4613
|
+
const x1 = round10(x + 2);
|
|
4614
|
+
const x2 = round10(x + LEGEND_SWATCH_W - 2);
|
|
4615
|
+
const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
|
|
4616
|
+
return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>`;
|
|
4617
|
+
}
|
|
4618
|
+
function vacantSwatch(x, y) {
|
|
4619
|
+
const cx = round10(x + LEGEND_SWATCH_W / 2);
|
|
4620
|
+
const bx = round10(cx - 8);
|
|
4621
|
+
const by = round10(y - 5);
|
|
4622
|
+
return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/><rect x="${round10(bx + 2)}" y="${round10(by + 2)}" width="12" height="6" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/>`;
|
|
4623
|
+
}
|
|
4624
|
+
function orgChartLayoutSvg(layout, opts = {}) {
|
|
4625
|
+
const labels = opts.labels ?? ORG_CHART_SVG_LABELS_EN;
|
|
4626
|
+
const parts = [];
|
|
4627
|
+
for (const el of layout.elements) parts.push(elementSvg5(el));
|
|
4628
|
+
for (const n of layout.nodes) parts.push(boxSvg(n));
|
|
4629
|
+
let width = layout.width;
|
|
4630
|
+
let height = layout.height;
|
|
4631
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
4632
|
+
const hasSolid = layout.elements.some((e) => e.kind === "stem" || e.kind === "bus" || e.kind === "drop");
|
|
4633
|
+
const hasAssistant = layout.nodes.some((n) => n.isAssistant);
|
|
4634
|
+
const hasDotted = layout.elements.some((e) => e.kind === "dotted");
|
|
4635
|
+
const hasVacant = layout.nodes.some((n) => n.style === "dashed");
|
|
4636
|
+
const entries = [];
|
|
4637
|
+
if (hasSolid) entries.push({ swatch: lineSwatch, label: labels.legend.line });
|
|
4638
|
+
if (hasAssistant) entries.push({ swatch: assistantSwatch, label: labels.legend.assistant });
|
|
4639
|
+
if (hasDotted) entries.push({ swatch: dottedSwatch, label: labels.legend.dotted });
|
|
4640
|
+
if (hasVacant) entries.push({ swatch: vacantSwatch, label: labels.legend.vacant });
|
|
4641
|
+
const block = legendBlock(entries, layout.height);
|
|
4642
|
+
if (block.svg !== "") {
|
|
4643
|
+
parts.push(block.svg);
|
|
4644
|
+
width = Math.max(width, block.width);
|
|
4645
|
+
height = block.height;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
const w = Math.ceil(width);
|
|
4649
|
+
const h = Math.ceil(height);
|
|
4650
|
+
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>`;
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
// src/org-chart/render.ts
|
|
4654
|
+
function orgChartSvg(input, opts = {}) {
|
|
4655
|
+
const layout = computeOrgChartLayout(input, {
|
|
4656
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
4657
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
4658
|
+
});
|
|
4659
|
+
const svg = orgChartLayoutSvg(layout, {
|
|
4660
|
+
...opts.legend === false ? { legend: false } : {},
|
|
4661
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
4662
|
+
});
|
|
4663
|
+
return { svg, layout };
|
|
4664
|
+
}
|
|
4665
|
+
|
|
2384
4666
|
exports.CHAR_W = CHAR_W;
|
|
2385
4667
|
exports.CODE_FONT = CODE_FONT;
|
|
2386
4668
|
exports.ECOMAP_LABELS_EN = ECOMAP_LABELS_EN;
|
|
@@ -2412,9 +4694,51 @@ exports.LEGEND_GAP = LEGEND_GAP;
|
|
|
2412
4694
|
exports.LEGEND_PAD = LEGEND_PAD;
|
|
2413
4695
|
exports.LEGEND_ROW_H = LEGEND_ROW_H;
|
|
2414
4696
|
exports.LEGEND_SWATCH_W = LEGEND_SWATCH_W;
|
|
4697
|
+
exports.LIFE_STATUSES = LIFE_STATUSES;
|
|
4698
|
+
exports.MAX_CONDITIONS_PER_INDIVIDUAL = MAX_CONDITIONS_PER_INDIVIDUAL;
|
|
2415
4699
|
exports.NODE_SIZE = NODE_SIZE;
|
|
4700
|
+
exports.ORG_ASSIST_ID_BASE = ORG_ASSIST_ID_BASE;
|
|
4701
|
+
exports.ORG_BUS_ID_BASE = ORG_BUS_ID_BASE;
|
|
4702
|
+
exports.ORG_CHART_SVG_LABELS_EN = ORG_CHART_SVG_LABELS_EN;
|
|
4703
|
+
exports.ORG_CHART_TITLE_LABELS_EN = ORG_CHART_TITLE_LABELS_EN;
|
|
4704
|
+
exports.ORG_DOTTED_ID_BASE = ORG_DOTTED_ID_BASE;
|
|
4705
|
+
exports.ORG_DROP_ID_BASE = ORG_DROP_ID_BASE;
|
|
4706
|
+
exports.ORG_LABEL_FONT = ORG_LABEL_FONT;
|
|
4707
|
+
exports.ORG_LABEL_LINE_H = ORG_LABEL_LINE_H;
|
|
4708
|
+
exports.ORG_MAX_MATRIX_EDGES_PER_NODE = ORG_MAX_MATRIX_EDGES_PER_NODE;
|
|
4709
|
+
exports.ORG_REPORT_KINDS = ORG_REPORT_KINDS;
|
|
4710
|
+
exports.ORG_STEM_ID_BASE = ORG_STEM_ID_BASE;
|
|
4711
|
+
exports.ORG_TITLE_FONT = ORG_TITLE_FONT;
|
|
4712
|
+
exports.ORG_VACANCIES = ORG_VACANCIES;
|
|
4713
|
+
exports.OrgChartValidationError = OrgChartValidationError;
|
|
2416
4714
|
exports.PARENT_REL_ID_BASE = PARENT_REL_ID_BASE;
|
|
4715
|
+
exports.PEDIGREE_SVG_LABELS_EN = PEDIGREE_SVG_LABELS_EN;
|
|
4716
|
+
exports.PEDIGREE_TITLE_LABELS_EN = PEDIGREE_TITLE_LABELS_EN;
|
|
4717
|
+
exports.PED_ADDRESS_FONT = PED_ADDRESS_FONT;
|
|
4718
|
+
exports.PED_CONDITION_FILLS = PED_CONDITION_FILLS;
|
|
4719
|
+
exports.PED_DESCENT_ID_BASE = PED_DESCENT_ID_BASE;
|
|
4720
|
+
exports.PED_GLYPH = PED_GLYPH;
|
|
4721
|
+
exports.PED_LABEL_FONT = PED_LABEL_FONT;
|
|
4722
|
+
exports.PED_LABEL_GAP = PED_LABEL_GAP;
|
|
4723
|
+
exports.PED_LABEL_LINE_H = PED_LABEL_LINE_H;
|
|
4724
|
+
exports.PED_MATING_ID_BASE = PED_MATING_ID_BASE;
|
|
4725
|
+
exports.PED_RISER_ID_BASE = PED_RISER_ID_BASE;
|
|
4726
|
+
exports.PED_SIBBAR_ID_BASE = PED_SIBBAR_ID_BASE;
|
|
4727
|
+
exports.PED_TWINBAR_ID_BASE = PED_TWINBAR_ID_BASE;
|
|
4728
|
+
exports.PHYLO_BRANCH_ID_BASE = PHYLO_BRANCH_ID_BASE;
|
|
4729
|
+
exports.PHYLO_CLADEBAR_ID_BASE = PHYLO_CLADEBAR_ID_BASE;
|
|
4730
|
+
exports.PHYLO_EXTENSION_ID_BASE = PHYLO_EXTENSION_ID_BASE;
|
|
4731
|
+
exports.PHYLO_LABEL_FONT = PHYLO_LABEL_FONT;
|
|
4732
|
+
exports.PHYLO_LABEL_GAP = PHYLO_LABEL_GAP;
|
|
4733
|
+
exports.PHYLO_ROOTSTUB_ID_BASE = PHYLO_ROOTSTUB_ID_BASE;
|
|
4734
|
+
exports.PHYLO_ROW_SLOT = PHYLO_ROW_SLOT;
|
|
4735
|
+
exports.PHYLO_SCALEBAR_ID = PHYLO_SCALEBAR_ID;
|
|
4736
|
+
exports.PHYLO_SUPPORT_FONT = PHYLO_SUPPORT_FONT;
|
|
4737
|
+
exports.PHYLO_SVG_LABELS_EN = PHYLO_SVG_LABELS_EN;
|
|
4738
|
+
exports.PHYLO_TITLE_LABELS_EN = PHYLO_TITLE_LABELS_EN;
|
|
2417
4739
|
exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
|
|
4740
|
+
exports.PedigreeValidationError = PedigreeValidationError;
|
|
4741
|
+
exports.PhyloValidationError = PhyloValidationError;
|
|
2418
4742
|
exports.QUALITY_LEXICON_EN = QUALITY_LEXICON_EN;
|
|
2419
4743
|
exports.UNION_NOTATION = UNION_NOTATION;
|
|
2420
4744
|
exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
|
|
@@ -2423,6 +4747,9 @@ exports.clampLabel = clampLabel;
|
|
|
2423
4747
|
exports.classifyRelationshipType = classifyRelationshipType;
|
|
2424
4748
|
exports.computeFaultTreeLayout = computeFaultTreeLayout;
|
|
2425
4749
|
exports.computeGenogramLayout = computeGenogramLayout;
|
|
4750
|
+
exports.computeOrgChartLayout = computeOrgChartLayout;
|
|
4751
|
+
exports.computePedigreeLayout = computePedigreeLayout;
|
|
4752
|
+
exports.computePhyloLayout = computePhyloLayout;
|
|
2426
4753
|
exports.ecomapSvg = ecomapSvg;
|
|
2427
4754
|
exports.estimateTextWidth = estimateTextWidth;
|
|
2428
4755
|
exports.faultTreeIssues = faultTreeIssues;
|
|
@@ -2433,11 +4760,26 @@ exports.genogramLayoutSvg = genogramLayoutSvg;
|
|
|
2433
4760
|
exports.genogramSvg = genogramSvg;
|
|
2434
4761
|
exports.latestUnionPerPair = latestUnionPerPair;
|
|
2435
4762
|
exports.legendBlock = legendBlock;
|
|
4763
|
+
exports.niceScaleStep = niceScaleStep;
|
|
2436
4764
|
exports.normalizeText = normalizeText;
|
|
4765
|
+
exports.orgChartIssues = orgChartIssues;
|
|
4766
|
+
exports.orgChartLayoutSvg = orgChartLayoutSvg;
|
|
4767
|
+
exports.orgChartSvg = orgChartSvg;
|
|
4768
|
+
exports.packSubtree = packSubtree;
|
|
2437
4769
|
exports.pathData = pathData;
|
|
4770
|
+
exports.pedigreeIssues = pedigreeIssues;
|
|
4771
|
+
exports.pedigreeLayoutSvg = pedigreeLayoutSvg;
|
|
4772
|
+
exports.pedigreeSvg = pedigreeSvg;
|
|
4773
|
+
exports.phyloIssues = phyloIssues;
|
|
4774
|
+
exports.phyloLayoutSvg = phyloLayoutSvg;
|
|
4775
|
+
exports.phyloSvg = phyloSvg;
|
|
2438
4776
|
exports.qualityLineStyle = qualityLineStyle;
|
|
2439
4777
|
exports.relationshipTypeTokens = relationshipTypeTokens;
|
|
4778
|
+
exports.romanNumeral = romanNumeral;
|
|
2440
4779
|
exports.validateFaultTree = validateFaultTree;
|
|
4780
|
+
exports.validateOrgChart = validateOrgChart;
|
|
4781
|
+
exports.validatePedigree = validatePedigree;
|
|
4782
|
+
exports.validatePhylo = validatePhylo;
|
|
2441
4783
|
exports.wrapLabel = wrapLabel;
|
|
2442
4784
|
exports.wrapLabelBalanced = wrapLabelBalanced;
|
|
2443
4785
|
exports.xmlEscape = xmlEscape;
|