compasso 0.2.0 → 0.3.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 +70 -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-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 +1616 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -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-iZjijjtK.d.cts +64 -0
- package/dist/labels-iZjijjtK.d.ts +64 -0
- package/dist/locales/pt-br.cjs +58 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +9 -2
- package/dist/locales/pt-br.d.ts +9 -2
- package/dist/locales/pt-br.js +55 -1
- package/dist/locales/pt-br.js.map +1 -1
- 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 +34 -12
- 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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { estimateTextWidth, clampLabel, legendBlock, xmlEscape, FONT_FAMILY, LEGEND_SWATCH_W } from './chunk-O3BT2O42.js';
|
|
2
|
+
|
|
3
|
+
// src/phylo/labels.ts
|
|
4
|
+
var PHYLO_TITLE_LABELS_EN = {
|
|
5
|
+
branchLength: "branch length",
|
|
6
|
+
support: "support",
|
|
7
|
+
clade: "clade",
|
|
8
|
+
tip: "tip",
|
|
9
|
+
root: "root"
|
|
10
|
+
};
|
|
11
|
+
var PHYLO_SVG_LABELS_EN = {
|
|
12
|
+
support: "Support value (bootstrap/posterior)",
|
|
13
|
+
scaleBar: "Scale: substitutions per site",
|
|
14
|
+
alignedTip: "Aligned tip (true position dotted)",
|
|
15
|
+
ariaLabel: {
|
|
16
|
+
cladogram: "Phylogenetic tree (cladogram)",
|
|
17
|
+
phylogram: "Phylogenetic tree (phylogram)"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/phylo/validate.ts
|
|
22
|
+
var PhyloValidationError = class extends Error {
|
|
23
|
+
issues;
|
|
24
|
+
constructor(issues) {
|
|
25
|
+
super(`invalid phylo tree: ${issues.map((i) => i.message).join("; ")}`);
|
|
26
|
+
this.name = "PhyloValidationError";
|
|
27
|
+
this.issues = issues;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function sortIssues(issues) {
|
|
31
|
+
const unique = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
|
|
33
|
+
return [...unique.values()].sort(
|
|
34
|
+
(a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function phyloIssues(input) {
|
|
38
|
+
if (input.nodes.length === 0 && input.edges.length === 0) return [];
|
|
39
|
+
const issues = [];
|
|
40
|
+
const push = (code, message) => {
|
|
41
|
+
issues.push({ code, message });
|
|
42
|
+
};
|
|
43
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
44
|
+
const dupNodeIds = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const n of input.nodes) {
|
|
46
|
+
if (nodeById.has(n.id)) dupNodeIds.add(n.id);
|
|
47
|
+
else nodeById.set(n.id, n);
|
|
48
|
+
}
|
|
49
|
+
for (const id of [...dupNodeIds].sort((a, b) => a - b)) {
|
|
50
|
+
push("duplicate-id", `duplicate node id ${id}`);
|
|
51
|
+
}
|
|
52
|
+
const dupEdgeIds = /* @__PURE__ */ new Set();
|
|
53
|
+
const seenEdgeIds = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const e of input.edges) {
|
|
55
|
+
if (seenEdgeIds.has(e.id)) dupEdgeIds.add(e.id);
|
|
56
|
+
else seenEdgeIds.add(e.id);
|
|
57
|
+
}
|
|
58
|
+
for (const id of [...dupEdgeIds].sort((a, b) => a - b)) {
|
|
59
|
+
push("duplicate-id", `duplicate edge id ${id}`);
|
|
60
|
+
}
|
|
61
|
+
if (issues.length > 0) {
|
|
62
|
+
return sortIssues(issues);
|
|
63
|
+
}
|
|
64
|
+
if (!nodeById.has(input.rootId)) {
|
|
65
|
+
push("unknown-root", `rootId ${input.rootId} is not a declared node`);
|
|
66
|
+
}
|
|
67
|
+
const incomingByChild = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
69
|
+
const knownParent = nodeById.has(e.parentId);
|
|
70
|
+
const knownChild = nodeById.has(e.childId);
|
|
71
|
+
if (!knownParent) push("unknown-endpoint", `edge ${e.id} parentId ${e.parentId} is not a declared node`);
|
|
72
|
+
if (!knownChild) push("unknown-endpoint", `edge ${e.id} childId ${e.childId} is not a declared node`);
|
|
73
|
+
if (e.length !== null && (!Number.isFinite(e.length) || e.length < 0)) {
|
|
74
|
+
push("negative-length", `edge ${e.id} has non-finite or negative length ${e.length}`);
|
|
75
|
+
}
|
|
76
|
+
if (knownParent && knownChild) {
|
|
77
|
+
const arr = incomingByChild.get(e.childId) ?? [];
|
|
78
|
+
arr.push(e.parentId);
|
|
79
|
+
incomingByChild.set(e.childId, arr);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const childIdsWithEdges = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const e of input.edges) childIdsWithEdges.add(e.childId);
|
|
84
|
+
for (const [childId, parents] of [...incomingByChild.entries()].sort((a, b) => a[0] - b[0])) {
|
|
85
|
+
if (parents.length > 1) {
|
|
86
|
+
push("multiple-parents", `node ${childId} has ${parents.length} parents`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const hasChildren = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const e of input.edges) {
|
|
91
|
+
if (nodeById.has(e.parentId)) hasChildren.add(e.parentId);
|
|
92
|
+
}
|
|
93
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
94
|
+
if (n.isTip === true && hasChildren.has(n.id)) {
|
|
95
|
+
push("tip-with-children", `node ${n.id} is declared a tip but has children`);
|
|
96
|
+
} else if (n.isTip === false && !hasChildren.has(n.id)) {
|
|
97
|
+
push("tip-with-children", `node ${n.id} is declared internal but has no children`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const GRAPH_BLOCKING = /* @__PURE__ */ new Set([
|
|
101
|
+
"duplicate-id",
|
|
102
|
+
"unknown-endpoint",
|
|
103
|
+
"unknown-root",
|
|
104
|
+
"multiple-parents"
|
|
105
|
+
]);
|
|
106
|
+
if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
|
|
107
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
109
|
+
const arr = childrenOf.get(e.parentId) ?? [];
|
|
110
|
+
arr.push(e.childId);
|
|
111
|
+
childrenOf.set(e.parentId, arr);
|
|
112
|
+
}
|
|
113
|
+
const color = /* @__PURE__ */ new Map();
|
|
114
|
+
const seenCycles = /* @__PURE__ */ new Set();
|
|
115
|
+
const dfs = (start) => {
|
|
116
|
+
const stack = [{ id: start, nextChild: 0 }];
|
|
117
|
+
color.set(start, 1);
|
|
118
|
+
while (stack.length > 0) {
|
|
119
|
+
const frame = stack[stack.length - 1];
|
|
120
|
+
const children = childrenOf.get(frame.id) ?? [];
|
|
121
|
+
if (frame.nextChild >= children.length) {
|
|
122
|
+
color.set(frame.id, 2);
|
|
123
|
+
stack.pop();
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const child = children[frame.nextChild];
|
|
127
|
+
frame.nextChild += 1;
|
|
128
|
+
const c = color.get(child) ?? 0;
|
|
129
|
+
if (c === 1) {
|
|
130
|
+
const from = stack.findIndex((f) => f.id === child);
|
|
131
|
+
const cycle = stack.slice(from).map((f) => f.id);
|
|
132
|
+
const minIdx = cycle.indexOf(Math.min(...cycle));
|
|
133
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
134
|
+
const key = rotated.join(">");
|
|
135
|
+
if (!seenCycles.has(key)) {
|
|
136
|
+
seenCycles.add(key);
|
|
137
|
+
push("cycle", `cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`);
|
|
138
|
+
}
|
|
139
|
+
} else if (c === 0) {
|
|
140
|
+
color.set(child, 1);
|
|
141
|
+
stack.push({ id: child, nextChild: 0 });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
146
|
+
if ((color.get(n.id) ?? 0) === 0) dfs(n.id);
|
|
147
|
+
}
|
|
148
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
149
|
+
const queue = [input.rootId];
|
|
150
|
+
while (queue.length > 0) {
|
|
151
|
+
const id = queue.shift();
|
|
152
|
+
if (reachable.has(id)) continue;
|
|
153
|
+
reachable.add(id);
|
|
154
|
+
for (const childId of childrenOf.get(id) ?? []) queue.push(childId);
|
|
155
|
+
}
|
|
156
|
+
for (const n of [...nodeById.values()].sort((a, b) => a.id - b.id)) {
|
|
157
|
+
if (!reachable.has(n.id)) push("disconnected-node", `node ${n.id} is unreachable from the root`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return sortIssues(issues);
|
|
161
|
+
}
|
|
162
|
+
function validatePhylo(input) {
|
|
163
|
+
const issues = phyloIssues(input);
|
|
164
|
+
if (issues.length > 0) throw new PhyloValidationError(issues);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/phylo/layout.ts
|
|
168
|
+
var PHYLO_LABEL_FONT = 12;
|
|
169
|
+
var PHYLO_ROW_SLOT = 22;
|
|
170
|
+
var PHYLO_SUPPORT_FONT = 10;
|
|
171
|
+
var PHYLO_LABEL_GAP = 8;
|
|
172
|
+
var PADDING = 32;
|
|
173
|
+
var PHYLO_LEVEL_W = 72;
|
|
174
|
+
var PHYLO_TIP_R = 2.5;
|
|
175
|
+
var SUPPORT_GAP = 4;
|
|
176
|
+
var SCALEBAR_ZONE = 34;
|
|
177
|
+
var MIN_DRAW_W = PHYLO_LEVEL_W;
|
|
178
|
+
var PHYLO_ZERO_NUDGE = 1;
|
|
179
|
+
var PHYLO_CLADEBAR_ID_BASE = 1e6;
|
|
180
|
+
var PHYLO_BRANCH_ID_BASE = 2e6;
|
|
181
|
+
var PHYLO_EXTENSION_ID_BASE = 3e6;
|
|
182
|
+
var PHYLO_ROOTSTUB_ID_BASE = 4e6;
|
|
183
|
+
var PHYLO_SCALEBAR_ID = 5e6;
|
|
184
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
185
|
+
function niceScaleStep(maxLen) {
|
|
186
|
+
if (!(maxLen > 0)) return 0;
|
|
187
|
+
const target = maxLen / 4;
|
|
188
|
+
const exp = Math.floor(Math.log10(target));
|
|
189
|
+
const pow = Math.pow(10, exp);
|
|
190
|
+
const tol = target * 1e-9;
|
|
191
|
+
for (const mult of [5, 2, 1]) {
|
|
192
|
+
const step = mult * pow;
|
|
193
|
+
if (step <= target + tol) return step;
|
|
194
|
+
}
|
|
195
|
+
return 5 * Math.pow(10, exp - 1);
|
|
196
|
+
}
|
|
197
|
+
function trimNumber(n) {
|
|
198
|
+
if (Number.isInteger(n)) return String(n);
|
|
199
|
+
return String(Number(n.toPrecision(6))).replace(/0+$/, "").replace(/\.$/, "");
|
|
200
|
+
}
|
|
201
|
+
function nodeTitle(node, isTip, isRoot, inLength, labels) {
|
|
202
|
+
if (node.title !== void 0) return node.title;
|
|
203
|
+
const role = isRoot ? labels.root : isTip ? labels.tip : labels.clade;
|
|
204
|
+
const head = node.label === "" ? role : node.label;
|
|
205
|
+
const parts = [head];
|
|
206
|
+
if (inLength !== null) parts.push(`${labels.branchLength}: ${trimNumber(inLength)}`);
|
|
207
|
+
if (!isTip && node.support !== void 0 && node.support !== null) {
|
|
208
|
+
parts.push(`${labels.support}: ${trimNumber(node.support)}`);
|
|
209
|
+
}
|
|
210
|
+
return parts.join(" \xB7 ");
|
|
211
|
+
}
|
|
212
|
+
function computePhyloLayout(input, opts = {}) {
|
|
213
|
+
const mode = opts.mode ?? "cladogram";
|
|
214
|
+
if (input.nodes.length === 0 && input.edges.length === 0) {
|
|
215
|
+
return { width: PADDING * 2, height: PADDING * 2, mode, nodes: [], elements: [], scaleBar: null, showSupport: false };
|
|
216
|
+
}
|
|
217
|
+
validatePhylo(input);
|
|
218
|
+
const titleLabels = opts.titleLabels ?? PHYLO_TITLE_LABELS_EN;
|
|
219
|
+
const alignTips = opts.alignTips ?? mode === "cladogram";
|
|
220
|
+
const showSupport = opts.showSupport ?? false;
|
|
221
|
+
const wantScaleBar = (opts.scaleBar ?? true) && mode === "phylogram";
|
|
222
|
+
const nodeById = new Map(input.nodes.map((n) => [n.id, n]));
|
|
223
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
224
|
+
const inLengthByChild = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
|
|
226
|
+
(childrenOf.get(e.parentId) ?? childrenOf.set(e.parentId, []).get(e.parentId)).push(e.childId);
|
|
227
|
+
inLengthByChild.set(e.childId, e.length);
|
|
228
|
+
}
|
|
229
|
+
const allWork = [];
|
|
230
|
+
const build = (nodeId, depth, dist) => {
|
|
231
|
+
const node = nodeById.get(nodeId);
|
|
232
|
+
const inLength = inLengthByChild.get(nodeId) ?? null;
|
|
233
|
+
const work = { node, children: [], depth, dist, inLength, cx: 0, cy: 0 };
|
|
234
|
+
allWork.push(work);
|
|
235
|
+
for (const childId of childrenOf.get(nodeId) ?? []) {
|
|
236
|
+
const childLen = inLengthByChild.get(childId);
|
|
237
|
+
const add = childLen === null || childLen === void 0 ? 0 : Math.max(0, childLen);
|
|
238
|
+
work.children.push(build(childId, depth + 1, dist + add));
|
|
239
|
+
}
|
|
240
|
+
return work;
|
|
241
|
+
};
|
|
242
|
+
const root = build(input.rootId, 0, 0);
|
|
243
|
+
const isTip = (w) => w.children.length === 0;
|
|
244
|
+
const top = PADDING;
|
|
245
|
+
let tipIndex = 0;
|
|
246
|
+
const assignY = (w) => {
|
|
247
|
+
if (isTip(w)) {
|
|
248
|
+
w.cy = top + tipIndex * PHYLO_ROW_SLOT + PHYLO_ROW_SLOT / 2;
|
|
249
|
+
tipIndex += 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const c of w.children) assignY(c);
|
|
253
|
+
const ys = w.children.map((c) => c.cy);
|
|
254
|
+
w.cy = (Math.min(...ys) + Math.max(...ys)) / 2;
|
|
255
|
+
};
|
|
256
|
+
assignY(root);
|
|
257
|
+
const tipCount = tipIndex;
|
|
258
|
+
const maxDepth = allWork.reduce((m, w) => Math.max(m, w.depth), 0);
|
|
259
|
+
const maxDist = allWork.reduce((m, w) => Math.max(m, w.dist), 0);
|
|
260
|
+
const supportNodes = allWork.filter(
|
|
261
|
+
(w) => !isTip(w) && w.node.support !== void 0 && w.node.support !== null
|
|
262
|
+
);
|
|
263
|
+
const effectiveShowSupport = showSupport && supportNodes.length > 0;
|
|
264
|
+
const supportMargin = effectiveShowSupport ? supportNodes.reduce(
|
|
265
|
+
(m, w) => Math.max(m, estimateTextWidth(trimNumber(w.node.support), PHYLO_SUPPORT_FONT)),
|
|
266
|
+
0
|
|
267
|
+
) + SUPPORT_GAP : 0;
|
|
268
|
+
const padLeft = PADDING + supportMargin;
|
|
269
|
+
const tipLabelW = allWork.filter(isTip).reduce((m, w) => Math.max(m, estimateTextWidth(clampLabel(w.node.label, opts.maxLabelChars), PHYLO_LABEL_FONT)), 0);
|
|
270
|
+
const labelReserve = (tipCount > 0 ? PHYLO_LABEL_GAP + tipLabelW : 0) + PADDING;
|
|
271
|
+
const phylogramScaled = mode === "phylogram" && maxDist > 0;
|
|
272
|
+
const drawW = Math.max(MIN_DRAW_W, maxDepth * PHYLO_LEVEL_W);
|
|
273
|
+
const scale = phylogramScaled ? drawW / maxDist : 0;
|
|
274
|
+
const xOf = (w) => {
|
|
275
|
+
if (phylogramScaled) return padLeft + w.dist * scale;
|
|
276
|
+
return padLeft + w.depth * PHYLO_LEVEL_W;
|
|
277
|
+
};
|
|
278
|
+
for (const w of allWork) w.cx = xOf(w);
|
|
279
|
+
if (phylogramScaled) {
|
|
280
|
+
const nudge = (w) => {
|
|
281
|
+
for (const c of w.children) {
|
|
282
|
+
if (!isTip(c) && c.cx <= w.cx + 1e-6) c.cx = w.cx + PHYLO_ZERO_NUDGE;
|
|
283
|
+
nudge(c);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
nudge(root);
|
|
287
|
+
}
|
|
288
|
+
const tipsAligned = alignTips;
|
|
289
|
+
const maxTipX = allWork.filter(isTip).reduce((m, w) => Math.max(m, w.cx), padLeft);
|
|
290
|
+
const alignX = phylogramScaled ? maxTipX : padLeft + maxDepth * PHYLO_LEVEL_W;
|
|
291
|
+
const tipDrawX = (w) => tipsAligned ? alignX : w.cx;
|
|
292
|
+
const rightmost = tipsAligned ? alignX : allWork.reduce((m, w) => Math.max(m, w.cx), padLeft);
|
|
293
|
+
const treeBottom = top + (tipCount > 0 ? tipCount * PHYLO_ROW_SLOT : PHYLO_ROW_SLOT);
|
|
294
|
+
const scaleBarPx = wantScaleBar && maxDist > 0 ? niceScaleStep(maxDist) * scale : 0;
|
|
295
|
+
const hasScaleBar = wantScaleBar && maxDist > 0 && scaleBarPx > 0;
|
|
296
|
+
const width = Math.ceil(rightmost + labelReserve);
|
|
297
|
+
const height = Math.ceil(treeBottom + (hasScaleBar ? SCALEBAR_ZONE : 0) + PADDING / 2);
|
|
298
|
+
const nodes = [];
|
|
299
|
+
const elements = [];
|
|
300
|
+
const branchTitle = (w) => {
|
|
301
|
+
if (w.inLength !== null) return `${titleLabels.branchLength}: ${trimNumber(w.inLength)}`;
|
|
302
|
+
return titleLabels.branchLength;
|
|
303
|
+
};
|
|
304
|
+
const ROOT_STUB = 14;
|
|
305
|
+
const rootDrawX = isTip(root) ? tipDrawX(root) : root.cx;
|
|
306
|
+
elements.push({
|
|
307
|
+
edgeId: PHYLO_ROOTSTUB_ID_BASE + root.node.id,
|
|
308
|
+
kind: "root-stub",
|
|
309
|
+
points: [
|
|
310
|
+
{ x: round(Math.max(PADDING / 2, rootDrawX - ROOT_STUB)), y: round(root.cy) },
|
|
311
|
+
{ x: round(rootDrawX), y: round(root.cy) }
|
|
312
|
+
],
|
|
313
|
+
dotted: false,
|
|
314
|
+
length: null,
|
|
315
|
+
title: titleLabels.root
|
|
316
|
+
});
|
|
317
|
+
const emit = (w) => {
|
|
318
|
+
const tip = isTip(w);
|
|
319
|
+
const isRoot = w === root;
|
|
320
|
+
const drawX = tip ? tipDrawX(w) : w.cx;
|
|
321
|
+
const support = w.node.support === void 0 ? null : w.node.support;
|
|
322
|
+
const labelLine = tip && w.node.label !== "" ? [clampLabel(w.node.label, opts.maxLabelChars)] : [];
|
|
323
|
+
nodes.push({
|
|
324
|
+
nodeId: w.node.id,
|
|
325
|
+
isTip: tip,
|
|
326
|
+
cx: round(drawX),
|
|
327
|
+
cy: round(w.cy),
|
|
328
|
+
support,
|
|
329
|
+
labelLines: labelLine,
|
|
330
|
+
labelLeft: round(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
|
|
331
|
+
depth: w.depth,
|
|
332
|
+
title: nodeTitle(w.node, tip, isRoot, w.inLength, titleLabels)
|
|
333
|
+
});
|
|
334
|
+
if (tip && tipsAligned && w.cx < alignX - 1e-6) {
|
|
335
|
+
elements.push({
|
|
336
|
+
edgeId: PHYLO_EXTENSION_ID_BASE + w.node.id,
|
|
337
|
+
kind: "extension",
|
|
338
|
+
points: [
|
|
339
|
+
{ x: round(w.cx), y: round(w.cy) },
|
|
340
|
+
{ x: round(alignX), y: round(w.cy) }
|
|
341
|
+
],
|
|
342
|
+
dotted: true,
|
|
343
|
+
length: null,
|
|
344
|
+
title: titleLabels.tip
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (w.children.length === 0) return;
|
|
348
|
+
const childYs = w.children.map((c) => c.cy);
|
|
349
|
+
const barTop = Math.min(...childYs);
|
|
350
|
+
const barBottom = Math.max(...childYs);
|
|
351
|
+
if (barBottom - barTop > 1e-6) {
|
|
352
|
+
elements.push({
|
|
353
|
+
edgeId: PHYLO_CLADEBAR_ID_BASE + w.node.id,
|
|
354
|
+
kind: "clade-bar",
|
|
355
|
+
points: [
|
|
356
|
+
{ x: round(w.cx), y: round(barTop) },
|
|
357
|
+
{ x: round(w.cx), y: round(barBottom) }
|
|
358
|
+
],
|
|
359
|
+
dotted: false,
|
|
360
|
+
length: null,
|
|
361
|
+
title: isRoot ? titleLabels.root : titleLabels.clade
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
for (const c of w.children) {
|
|
365
|
+
elements.push({
|
|
366
|
+
edgeId: PHYLO_BRANCH_ID_BASE + c.node.id,
|
|
367
|
+
kind: "branch",
|
|
368
|
+
points: [
|
|
369
|
+
{ x: round(w.cx), y: round(c.cy) },
|
|
370
|
+
{ x: round(c.cx), y: round(c.cy) }
|
|
371
|
+
],
|
|
372
|
+
dotted: false,
|
|
373
|
+
length: c.inLength,
|
|
374
|
+
title: branchTitle(c)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
for (const c of w.children) emit(c);
|
|
378
|
+
};
|
|
379
|
+
emit(root);
|
|
380
|
+
let scaleBar = null;
|
|
381
|
+
if (hasScaleBar) {
|
|
382
|
+
const stepLen = niceScaleStep(maxDist);
|
|
383
|
+
const barX = padLeft;
|
|
384
|
+
const barY = treeBottom + SCALEBAR_ZONE / 2;
|
|
385
|
+
scaleBar = {
|
|
386
|
+
length: stepLen,
|
|
387
|
+
x: round(barX),
|
|
388
|
+
y: round(barY),
|
|
389
|
+
pxLength: round(scaleBarPx),
|
|
390
|
+
valueLabel: trimNumber(stepLen)
|
|
391
|
+
};
|
|
392
|
+
elements.push({
|
|
393
|
+
edgeId: PHYLO_SCALEBAR_ID,
|
|
394
|
+
kind: "scale-bar",
|
|
395
|
+
points: [
|
|
396
|
+
{ x: round(barX), y: round(barY) },
|
|
397
|
+
{ x: round(barX + scaleBarPx), y: round(barY) }
|
|
398
|
+
],
|
|
399
|
+
dotted: false,
|
|
400
|
+
length: stepLen,
|
|
401
|
+
title: `${titleLabels.branchLength}: ${trimNumber(stepLen)}`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return { width, height, mode, nodes, elements, scaleBar, showSupport: effectiveShowSupport };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/phylo/svg.ts
|
|
408
|
+
var GLYPH_STROKE = "#52525b";
|
|
409
|
+
var LABEL_FILL = "#3f3f46";
|
|
410
|
+
var EDGE_INK = "#71717a";
|
|
411
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
412
|
+
function nodeSvg(n, showSupport) {
|
|
413
|
+
const pieces = [`<title>${xmlEscape(n.title)}</title>`];
|
|
414
|
+
pieces.push(`<circle cx="${n.cx}" cy="${n.cy}" r="2.5" fill="${GLYPH_STROKE}"/>`);
|
|
415
|
+
if (n.labelLines.length > 0) {
|
|
416
|
+
pieces.push(
|
|
417
|
+
`<text x="${n.labelLeft}" y="${round2(n.cy + PHYLO_LABEL_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_LABEL_FONT}" fill="${LABEL_FILL}">${xmlEscape(n.labelLines[0])}</text>`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (showSupport && !n.isTip && n.support !== null) {
|
|
421
|
+
pieces.push(
|
|
422
|
+
`<text x="${round2(n.cx - 4)}" y="${round2(n.cy - 3)}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL}">${xmlEscape(String(n.support))}</text>`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return `<g data-node-id="n${n.nodeId}">${pieces.join("")}</g>`;
|
|
426
|
+
}
|
|
427
|
+
function elementSvg(el) {
|
|
428
|
+
const a = el.points[0];
|
|
429
|
+
const b = el.points[1];
|
|
430
|
+
const dash = el.dotted ? ` stroke-dasharray="2,3"` : "";
|
|
431
|
+
const opacity = el.kind === "extension" ? "0.5" : "0.75";
|
|
432
|
+
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_INK}" stroke-width="1.5" stroke-opacity="${opacity}"${dash}/></g>`;
|
|
433
|
+
}
|
|
434
|
+
function scaleBarLabelSvg(layout) {
|
|
435
|
+
const bar = layout.scaleBar;
|
|
436
|
+
if (bar === null) return "";
|
|
437
|
+
const tick = (x) => `<line x1="${x}" y1="${round2(bar.y - 3)}" x2="${x}" y2="${round2(bar.y + 3)}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
438
|
+
return tick(bar.x) + tick(round2(bar.x + bar.pxLength)) + `<text x="${round2(bar.x + bar.pxLength / 2)}" y="${round2(bar.y - 6)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL}">${xmlEscape(bar.valueLabel)}</text>`;
|
|
439
|
+
}
|
|
440
|
+
function supportSwatch(x, y) {
|
|
441
|
+
const cx = round2(x + LEGEND_SWATCH_W / 2);
|
|
442
|
+
return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE}"/><text x="${round2(cx + 5)}" y="${round2(y + PHYLO_SUPPORT_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL}">95</text>`;
|
|
443
|
+
}
|
|
444
|
+
function scaleSwatch(x, y) {
|
|
445
|
+
return `<line x1="${round2(x)}" y1="${y}" x2="${round2(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.75"/>`;
|
|
446
|
+
}
|
|
447
|
+
function alignedTipSwatch(x, y) {
|
|
448
|
+
return `<line x1="${round2(x)}" y1="${y}" x2="${round2(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK}" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="2,3"/>`;
|
|
449
|
+
}
|
|
450
|
+
function phyloLayoutSvg(layout, opts = {}) {
|
|
451
|
+
const labels = opts.labels ?? PHYLO_SVG_LABELS_EN;
|
|
452
|
+
const showSupport = layout.showSupport;
|
|
453
|
+
const parts = [];
|
|
454
|
+
for (const el of layout.elements) parts.push(elementSvg(el));
|
|
455
|
+
parts.push(scaleBarLabelSvg(layout));
|
|
456
|
+
for (const n of layout.nodes) parts.push(nodeSvg(n, showSupport));
|
|
457
|
+
let width = layout.width;
|
|
458
|
+
let height = layout.height;
|
|
459
|
+
if (opts.legend !== false && layout.nodes.length > 0) {
|
|
460
|
+
const entries = [];
|
|
461
|
+
if (showSupport) entries.push({ swatch: supportSwatch, label: labels.support });
|
|
462
|
+
if (layout.scaleBar !== null) entries.push({ swatch: scaleSwatch, label: labels.scaleBar });
|
|
463
|
+
if (layout.elements.some((e) => e.kind === "extension")) {
|
|
464
|
+
entries.push({ swatch: alignedTipSwatch, label: labels.alignedTip });
|
|
465
|
+
}
|
|
466
|
+
const block = legendBlock(entries, layout.height);
|
|
467
|
+
if (block.svg !== "") {
|
|
468
|
+
parts.push(block.svg);
|
|
469
|
+
width = Math.max(width, block.width);
|
|
470
|
+
height = block.height;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const w = Math.ceil(width);
|
|
474
|
+
const h = Math.ceil(height);
|
|
475
|
+
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>`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/phylo/render.ts
|
|
479
|
+
function phyloSvg(input, opts = {}) {
|
|
480
|
+
const layout = computePhyloLayout(input, {
|
|
481
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {},
|
|
482
|
+
...opts.alignTips !== void 0 ? { alignTips: opts.alignTips } : {},
|
|
483
|
+
...opts.showSupport !== void 0 ? { showSupport: opts.showSupport } : {},
|
|
484
|
+
...opts.scaleBar !== void 0 ? { scaleBar: opts.scaleBar } : {},
|
|
485
|
+
...opts.maxLabelChars !== void 0 ? { maxLabelChars: opts.maxLabelChars } : {},
|
|
486
|
+
...opts.titleLabels !== void 0 ? { titleLabels: opts.titleLabels } : {}
|
|
487
|
+
});
|
|
488
|
+
const svg = phyloLayoutSvg(layout, {
|
|
489
|
+
...opts.legend === false ? { legend: false } : {},
|
|
490
|
+
...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
|
|
491
|
+
});
|
|
492
|
+
return { svg, layout };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export { PHYLO_BRANCH_ID_BASE, PHYLO_CLADEBAR_ID_BASE, PHYLO_EXTENSION_ID_BASE, PHYLO_LABEL_FONT, PHYLO_LABEL_GAP, PHYLO_ROOTSTUB_ID_BASE, PHYLO_ROW_SLOT, PHYLO_SCALEBAR_ID, PHYLO_SUPPORT_FONT, PHYLO_SVG_LABELS_EN, PHYLO_TITLE_LABELS_EN, PhyloValidationError, computePhyloLayout, niceScaleStep, phyloIssues, phyloLayoutSvg, phyloSvg, validatePhylo };
|
|
496
|
+
//# sourceMappingURL=chunk-JP4N42AY.js.map
|
|
497
|
+
//# sourceMappingURL=chunk-JP4N42AY.js.map
|