compasso 0.1.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 +126 -8
- 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-LRHHUJFZ.js +703 -0
- package/dist/chunk-LRHHUJFZ.js.map +1 -0
- package/dist/{chunk-E456YKAJ.js → chunk-O3BT2O42.js} +69 -10
- package/dist/chunk-O3BT2O42.js.map +1 -0
- package/dist/{chunk-L5CYESBI.js → chunk-Q6DVTCXD.js} +9 -24
- package/dist/chunk-Q6DVTCXD.js.map +1 -0
- package/dist/{chunk-5RRRE2GF.js → chunk-RWPGGWO5.js} +9 -28
- package/dist/chunk-RWPGGWO5.js.map +1 -0
- package/dist/chunk-ZBDABVIO.js +252 -0
- package/dist/chunk-ZBDABVIO.js.map +1 -0
- package/dist/core/index.cjs +74 -7
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +33 -29
- package/dist/core/index.d.ts +33 -29
- package/dist/core/index.js +1 -1
- package/dist/ecomap/index.cjs +43 -28
- package/dist/ecomap/index.cjs.map +1 -1
- package/dist/ecomap/index.js +2 -2
- package/dist/fault-tree/index.cjs +782 -0
- package/dist/fault-tree/index.cjs.map +1 -0
- package/dist/fault-tree/index.d.cts +148 -0
- package/dist/fault-tree/index.d.ts +148 -0
- package/dist/fault-tree/index.js +4 -0
- package/dist/fault-tree/index.js.map +1 -0
- package/dist/fishbone/index.cjs +314 -0
- package/dist/fishbone/index.cjs.map +1 -0
- package/dist/fishbone/index.d.cts +91 -0
- package/dist/fishbone/index.d.ts +91 -0
- package/dist/fishbone/index.js +4 -0
- package/dist/fishbone/index.js.map +1 -0
- package/dist/genogram/index.cjs +47 -32
- package/dist/genogram/index.cjs.map +1 -1
- package/dist/genogram/index.d.cts +7 -4
- package/dist/genogram/index.d.ts +7 -4
- package/dist/genogram/index.js +2 -2
- package/dist/index.cjs +2622 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +7 -3
- 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-CYbM5XV7.d.cts +83 -0
- package/dist/labels-CYbM5XV7.d.ts +83 -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 +94 -0
- package/dist/locales/pt-br.cjs.map +1 -1
- package/dist/locales/pt-br.d.cts +14 -2
- package/dist/locales/pt-br.d.ts +14 -2
- package/dist/locales/pt-br.js +88 -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/text-DuO_PwYw.d.cts +45 -0
- package/dist/text-DuO_PwYw.d.ts +45 -0
- package/dist/types-BnMG7TCd.d.cts +66 -0
- package/dist/types-BnMG7TCd.d.ts +66 -0
- package/dist/xml-DDae1eUr.d.cts +4 -0
- package/dist/xml-DDae1eUr.d.ts +4 -0
- package/package.json +100 -26
- package/dist/chunk-5RRRE2GF.js.map +0 -1
- package/dist/chunk-E456YKAJ.js.map +0 -1
- package/dist/chunk-L5CYESBI.js.map +0 -1
- package/dist/kinship-BARO5-qz.d.cts +0 -115
- package/dist/kinship-Bkf87Jhu.d.ts +0 -115
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { wrapLabelBalanced, clampLabel, estimateTextWidth, wrapLabel, xmlEscape, legendBlock, FONT_FAMILY, LEGEND_SWATCH_W } from './chunk-O3BT2O42.js';
|
|
2
|
+
|
|
3
|
+
// src/fishbone/render.ts
|
|
4
|
+
var FISHBONE_LABELS_EN = {
|
|
5
|
+
cause: "Cause",
|
|
6
|
+
subCause: "Sub-cause",
|
|
7
|
+
ariaLabel: "Cause-and-effect diagram (Ishikawa)"
|
|
8
|
+
};
|
|
9
|
+
var FishboneValidationError = class extends Error {
|
|
10
|
+
issues;
|
|
11
|
+
constructor(issues) {
|
|
12
|
+
super(`invalid fishbone: ${issues.map((i) => i.message).join("; ")}`);
|
|
13
|
+
this.name = "FishboneValidationError";
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function duplicateIds(ids) {
|
|
18
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19
|
+
const dups = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const id of ids) {
|
|
21
|
+
if (seen.has(id)) dups.add(id);
|
|
22
|
+
seen.add(id);
|
|
23
|
+
}
|
|
24
|
+
return [...dups].sort((a, b) => a - b);
|
|
25
|
+
}
|
|
26
|
+
function validateIds(input) {
|
|
27
|
+
const issues = [];
|
|
28
|
+
const collect = (noun, ids) => {
|
|
29
|
+
for (const id of duplicateIds(ids)) {
|
|
30
|
+
issues.push({ code: "duplicate-id", message: `duplicate ${noun} id ${id}` });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
collect("category", input.categories.map((c) => c.id));
|
|
34
|
+
collect("cause", input.categories.flatMap((c) => c.causes.map((k) => k.id)));
|
|
35
|
+
collect(
|
|
36
|
+
"sub-cause",
|
|
37
|
+
input.categories.flatMap((c) => c.causes.flatMap((k) => k.subCauses.map((s) => s.id)))
|
|
38
|
+
);
|
|
39
|
+
if (issues.length > 0) throw new FishboneValidationError(issues);
|
|
40
|
+
}
|
|
41
|
+
var TAN60 = 1.7320508075688772;
|
|
42
|
+
var SIN60 = 0.8660254037844386;
|
|
43
|
+
var COS60 = 0.5;
|
|
44
|
+
var PADDING = 32;
|
|
45
|
+
var ROW_GAP = 12;
|
|
46
|
+
var BONE_GAP = 36;
|
|
47
|
+
var CAT_PAD_X = 12;
|
|
48
|
+
var CAT_PAD_Y = 8;
|
|
49
|
+
var CAT_GAP = 12;
|
|
50
|
+
var TAIL_EXTRA = 44;
|
|
51
|
+
var HEAD_GAP = 24;
|
|
52
|
+
var HEAD_PAD_X = 14;
|
|
53
|
+
var HEAD_PAD_Y = 10;
|
|
54
|
+
var SUB_PER_LINE = 18;
|
|
55
|
+
var ASCENT_12 = 11;
|
|
56
|
+
var DESCENT_12 = 3;
|
|
57
|
+
function verticalMetrics(fontSize) {
|
|
58
|
+
const ascent = Math.ceil(ASCENT_12 * fontSize / 12);
|
|
59
|
+
const descent = Math.ceil(DESCENT_12 * fontSize / 12);
|
|
60
|
+
const lineH = ascent + descent;
|
|
61
|
+
const twigGap = descent + 1;
|
|
62
|
+
return {
|
|
63
|
+
ascent,
|
|
64
|
+
lineH,
|
|
65
|
+
twigGap,
|
|
66
|
+
sV: 2 * lineH + twigGap + 8,
|
|
67
|
+
spineClear: 2 * lineH + twigGap,
|
|
68
|
+
boneClear: Math.ceil((2 * lineH + twigGap) / TAN60) + 25,
|
|
69
|
+
subGap: lineH
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var EDGE_INK = "#71717a";
|
|
73
|
+
var BOX_STROKE = "#52525b";
|
|
74
|
+
var LABEL_FILL = "#3f3f46";
|
|
75
|
+
var SPINE_W = 2.5;
|
|
76
|
+
var SPINE_OP = 0.85;
|
|
77
|
+
var BONE_W = 2;
|
|
78
|
+
var BONE_OP = 0.8;
|
|
79
|
+
var TWIG_W = 1.5;
|
|
80
|
+
var TWIG_OP = 0.75;
|
|
81
|
+
var SUB_W = 1.2;
|
|
82
|
+
var SUB_OP = 0.7;
|
|
83
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
84
|
+
function arrowHead(tipX, tipY, ux, uy, opacity) {
|
|
85
|
+
const LEN = 9;
|
|
86
|
+
const HALF_W = 4.5;
|
|
87
|
+
const bx = tipX - ux * LEN;
|
|
88
|
+
const by = tipY - uy * LEN;
|
|
89
|
+
const px = -uy;
|
|
90
|
+
const py = ux;
|
|
91
|
+
const points = [
|
|
92
|
+
`${round(tipX)},${round(tipY)}`,
|
|
93
|
+
`${round(bx + px * HALF_W)},${round(by + py * HALF_W)}`,
|
|
94
|
+
`${round(bx - px * HALF_W)},${round(by - py * HALF_W)}`
|
|
95
|
+
].join(" ");
|
|
96
|
+
return `<polygon points="${points}" fill="${EDGE_INK}" fill-opacity="${opacity}"/>`;
|
|
97
|
+
}
|
|
98
|
+
function lineEl(x1, y1, x2, y2, w, op) {
|
|
99
|
+
return `<line x1="${round(x1)}" y1="${round(y1)}" x2="${round(x2)}" y2="${round(y2)}" stroke="${EDGE_INK}" stroke-width="${w}" stroke-opacity="${op}"/>`;
|
|
100
|
+
}
|
|
101
|
+
function fishboneSvg(input, opts = {}) {
|
|
102
|
+
validateIds(input);
|
|
103
|
+
const fontSize = opts.fontSize ?? 12;
|
|
104
|
+
const { ascent, lineH, twigGap, sV, spineClear, boneClear, subGap } = verticalMetrics(fontSize);
|
|
105
|
+
const labels = opts.labels ?? FISHBONE_LABELS_EN;
|
|
106
|
+
const arrows = opts.arrowheads !== false;
|
|
107
|
+
const bones = input.categories.map((category, idx) => {
|
|
108
|
+
let cursor = spineClear;
|
|
109
|
+
const bands = category.causes.map((cause) => {
|
|
110
|
+
const lines = wrapLabelBalanced(clampLabel(cause.label, opts.maxLabelChars));
|
|
111
|
+
const labelW = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize)));
|
|
112
|
+
const blockH = lines.length * lineH;
|
|
113
|
+
const offset = cursor;
|
|
114
|
+
const bx = -offset / TAN60;
|
|
115
|
+
const subs = [];
|
|
116
|
+
for (const sub of cause.subCauses) {
|
|
117
|
+
const line = wrapLabel(clampLabel(sub.label, opts.maxLabelChars), SUB_PER_LINE, 1)[0];
|
|
118
|
+
const w2 = estimateTextWidth(line, fontSize);
|
|
119
|
+
const prev = subs.length > 0 ? subs[subs.length - 1] : null;
|
|
120
|
+
const sx = prev === null ? bx - (sV + lineH) / TAN60 - 10 - w2 / 2 : prev.sx - (prev.w + w2) * 0.6 - subGap;
|
|
121
|
+
subs.push({ sub, line, w: w2, sx, outerX: sx - sV / TAN60 });
|
|
122
|
+
}
|
|
123
|
+
const last = subs.length > 0 ? subs[subs.length - 1] : null;
|
|
124
|
+
const subSpan = last === null ? 0 : bx - (last.outerX - last.w / 2);
|
|
125
|
+
const twigLen = last === null ? labelW + boneClear : Math.max(labelW + boneClear, subSpan + boneClear, labelW + (bx - last.outerX) + 10);
|
|
126
|
+
const bandH = Math.max(blockH + twigGap, subs.length > 0 ? sV + lineH + twigGap : 0) + ROW_GAP;
|
|
127
|
+
cursor += bandH;
|
|
128
|
+
return { cause, lines, labelW, blockH, offset, bandH, bx, freeEnd: bx - twigLen, subs };
|
|
129
|
+
});
|
|
130
|
+
const B = cursor + 16;
|
|
131
|
+
const catLines = wrapLabelBalanced(clampLabel(category.label, opts.maxLabelChars));
|
|
132
|
+
const boxW = Math.max(...catLines.map((l) => estimateTextWidth(l, fontSize))) + CAT_PAD_X * 2;
|
|
133
|
+
const boxH = catLines.length * lineH + CAT_PAD_Y * 2;
|
|
134
|
+
const tipX = -B / TAN60;
|
|
135
|
+
return {
|
|
136
|
+
category,
|
|
137
|
+
up: idx % 2 === 0,
|
|
138
|
+
bands,
|
|
139
|
+
B,
|
|
140
|
+
catLines,
|
|
141
|
+
boxW,
|
|
142
|
+
boxH,
|
|
143
|
+
// Left extent: the category box's left edge or the deepest twig free end —
|
|
144
|
+
// every label sits AT or right of its twig's free end, every sub label sits
|
|
145
|
+
// right of the free end too (twigLen ≥ subSpan + boneClear).
|
|
146
|
+
relL: Math.max(B / TAN60 + boxW / 2, ...bands.map((b) => -b.freeEnd)),
|
|
147
|
+
// Right extent: a wide category box on a short bone may overhang the attach.
|
|
148
|
+
relR: Math.max(0, boxW / 2 + tipX),
|
|
149
|
+
ax: 0
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const cursors = { top: 0, bottom: 0 };
|
|
153
|
+
for (const bone of bones) {
|
|
154
|
+
const side = bone.up ? "top" : "bottom";
|
|
155
|
+
bone.ax = cursors[side] - bone.relR;
|
|
156
|
+
cursors[side] = bone.ax - bone.relL - BONE_GAP;
|
|
157
|
+
}
|
|
158
|
+
const effLines = wrapLabelBalanced(clampLabel(input.effectLabel, opts.maxLabelChars));
|
|
159
|
+
const headW = Math.max(...effLines.map((l) => estimateTextWidth(l, fontSize))) + HEAD_PAD_X * 2;
|
|
160
|
+
const headH = effLines.length * lineH + HEAD_PAD_Y * 2;
|
|
161
|
+
const headLeft = bones.length > 0 ? HEAD_GAP : 0;
|
|
162
|
+
const tailX = bones.length > 0 ? Math.min(...bones.map((b) => b.ax - b.relL)) - TAIL_EXTRA : headLeft - (TAIL_EXTRA + HEAD_GAP);
|
|
163
|
+
let minY = -headH / 2;
|
|
164
|
+
let maxY = headH / 2;
|
|
165
|
+
for (const bone of bones) {
|
|
166
|
+
const reach = bone.B + CAT_GAP + bone.boxH;
|
|
167
|
+
if (bone.up) minY = Math.min(minY, -reach);
|
|
168
|
+
else maxY = Math.max(maxY, reach);
|
|
169
|
+
}
|
|
170
|
+
const dx = PADDING - tailX;
|
|
171
|
+
const dy = PADDING - minY;
|
|
172
|
+
let width = headLeft + headW - tailX + PADDING * 2;
|
|
173
|
+
let height = maxY - minY + PADDING * 2;
|
|
174
|
+
const spineY = dy;
|
|
175
|
+
const centeredYs = (cy, n) => Array.from({ length: n }, (_, i) => cy - (n - 1) * lineH / 2 + i * lineH + fontSize * 0.32);
|
|
176
|
+
const bandBaselines = (e, n, up) => Array.from(
|
|
177
|
+
{ length: n },
|
|
178
|
+
(_, k) => up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH
|
|
179
|
+
);
|
|
180
|
+
const textBlock = (anchor, x, ys, lines) => `<text text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">` + lines.map((line, i) => `<tspan x="${round(x)}" y="${round(ys[i])}">${xmlEscape(line)}</tspan>`).join("") + `</text>`;
|
|
181
|
+
const parts = [];
|
|
182
|
+
{
|
|
183
|
+
const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];
|
|
184
|
+
if (arrows) body.push(arrowHead(headLeft + dx, spineY, 1, 0, SPINE_OP));
|
|
185
|
+
parts.push(`<g data-edge-id="spine"><title>${xmlEscape(labels.ariaLabel)}</title>${body.join("")}</g>`);
|
|
186
|
+
}
|
|
187
|
+
for (const bone of bones) {
|
|
188
|
+
const sgn = bone.up ? -1 : 1;
|
|
189
|
+
const ax = bone.ax + dx;
|
|
190
|
+
const tipX = ax - bone.B / TAN60;
|
|
191
|
+
const body = [lineEl(tipX, spineY + sgn * bone.B, ax, spineY, BONE_W, BONE_OP)];
|
|
192
|
+
if (arrows) body.push(arrowHead(ax, spineY, COS60, -sgn * SIN60, BONE_OP));
|
|
193
|
+
const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;
|
|
194
|
+
body.push(
|
|
195
|
+
`<rect x="${round(tipX - bone.boxW / 2)}" y="${round(boxTop)}" width="${round(bone.boxW)}" height="${round(bone.boxH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="1.5"/>`
|
|
196
|
+
);
|
|
197
|
+
body.push(textBlock("middle", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));
|
|
198
|
+
parts.push(
|
|
199
|
+
`<g data-node-id="b${bone.category.id}"><title>${xmlEscape(bone.category.title ?? bone.category.label)}</title>${body.join("")}</g>`
|
|
200
|
+
);
|
|
201
|
+
for (const band of bone.bands) {
|
|
202
|
+
const ty = spineY + sgn * band.offset;
|
|
203
|
+
const bx = ax + band.bx;
|
|
204
|
+
const freeEnd = ax + band.freeEnd;
|
|
205
|
+
const cbody = [lineEl(freeEnd, ty, bx, ty, TWIG_W, TWIG_OP)];
|
|
206
|
+
if (arrows) cbody.push(arrowHead(bx, ty, 1, 0, TWIG_OP));
|
|
207
|
+
cbody.push(textBlock("start", freeEnd + 2, bandBaselines(band.offset, band.lines.length, bone.up), band.lines));
|
|
208
|
+
parts.push(
|
|
209
|
+
`<g data-node-id="c${band.cause.id}"><title>${xmlEscape(band.cause.title ?? band.cause.label)}</title>${cbody.join("")}</g>`
|
|
210
|
+
);
|
|
211
|
+
for (const s of band.subs) {
|
|
212
|
+
const sx = ax + s.sx;
|
|
213
|
+
const ox = ax + s.outerX;
|
|
214
|
+
const sbody = [lineEl(ox, spineY + sgn * (band.offset + sV), sx, ty, SUB_W, SUB_OP)];
|
|
215
|
+
if (arrows) sbody.push(arrowHead(sx, ty, COS60, -sgn * SIN60, SUB_OP));
|
|
216
|
+
const baseline = bone.up ? spineY - (band.offset + sV + twigGap) : spineY + band.offset + sV + twigGap + ascent;
|
|
217
|
+
sbody.push(textBlock("middle", ox, [baseline], [s.line]));
|
|
218
|
+
parts.push(`<g data-node-id="s${s.sub.id}"><title>${xmlEscape(s.sub.label)}</title>${sbody.join("")}</g>`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
{
|
|
223
|
+
const x = headLeft + dx;
|
|
224
|
+
parts.push(
|
|
225
|
+
`<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${round(x)}" y="${round(spineY - headH / 2)}" width="${round(headW)}" height="${round(headH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="2"/>` + textBlock("middle", x + headW / 2, centeredYs(spineY, effLines.length), effLines) + `</g>`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));
|
|
229
|
+
if (opts.legend !== false && anySubs) {
|
|
230
|
+
const entries = [
|
|
231
|
+
{
|
|
232
|
+
swatch: (x, y) => lineEl(x, y, x + LEGEND_SWATCH_W, y, TWIG_W, TWIG_OP),
|
|
233
|
+
label: labels.cause
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
swatch: (x, y) => lineEl(x, y + 5, x + LEGEND_SWATCH_W, y - 5, SUB_W, SUB_OP),
|
|
237
|
+
label: labels.subCause
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
const block = legendBlock(entries, height);
|
|
241
|
+
parts.push(block.svg);
|
|
242
|
+
width = Math.max(width, block.width);
|
|
243
|
+
height = block.height;
|
|
244
|
+
}
|
|
245
|
+
const w = Math.ceil(width);
|
|
246
|
+
const h = Math.ceil(height);
|
|
247
|
+
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>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { FISHBONE_LABELS_EN, FishboneValidationError, fishboneSvg };
|
|
251
|
+
//# sourceMappingURL=chunk-ZBDABVIO.js.map
|
|
252
|
+
//# sourceMappingURL=chunk-ZBDABVIO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fishbone/render.ts"],"names":["w"],"mappings":"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","file":"chunk-ZBDABVIO.js","sourcesContent":["// Ishikawa (fishbone / cause-and-effect) renderer — a horizontal spine running into\n// the effect head, category bones at 60°, horizontal cause twigs and ONE level of\n// diagonal sub-cause twigs — as a SELF-CONTAINED SVG string. Pure (data in, string\n// out), deterministic (same input → same SVG), no DOM, no dependencies.\n//\n// Notation (Ishikawa, *Guide to Quality Control*, JUSE 1976; ASQ template lineage):\n// the effect (\"characteristic\") is written in a box at the head of the central arrow;\n// big bones are diagonal, medium bones (causes) horizontal, small bones (sub-causes)\n// parallel to the big bone — every level drawn as an arrow converging on the effect.\n// The cartoon fish head is template folklore, not the standard, so the head is a\n// plain rectangle. Bones are at EXACTLY 60° to the spine (\"about 60°\" in the\n// literature): the angle is a constant, not an option, because every label-clearance\n// inequality below is derived from it. Beyond small bones the JUSE method itself\n// moves to why-why tables, so sub-sub-causes are typed away.\n//\n// DECLARED ORDER IS HONORED — the documented exception to the library's id-sorting\n// (ecomap) doctrine: the analyst's ordering is declared data (significant categories\n// nearest the head, alternating top/bottom; causes outward from the spine; sub-causes\n// from the bone outward). Numeric ids are still required — unique per namespace\n// (categories / causes / sub-causes), enforced by FishboneValidationError — because\n// the decoration hooks (`data-node-id`) must be unambiguous.\n//\n// HONESTY RULE: one declared category = one bone — no default 5M/6M skeleton, no\n// inferred grouping. Arrowheads are uniform notation (the diagram IS arrows\n// converging on the effect), so `arrowheads:false` toggles presentation, never data.\n//\n// Trig values are HARD-CODED literals: Math.tan/sin at runtime would make\n// byte-determinism hostage to engine-specific transcendental ulps.\n\nimport {\n FONT_FAMILY,\n LEGEND_SWATCH_W,\n clampLabel,\n estimateTextWidth,\n legendBlock,\n wrapLabel,\n wrapLabelBalanced,\n xmlEscape,\n type LegendEntry,\n} from \"../core\";\n\n// ── Input model ───────────────────────────────────────────────────────────────\n\nexport interface FishboneSubCause {\n id: number;\n /** Verbatim sub-cause text (drawn as one clamped line; full text in the <title>). */\n label: string;\n}\n\nexport interface FishboneCause {\n id: number;\n label: string;\n /** Exactly one sub-level is in scope (JUSE small bones); deeper nesting is typed away. */\n subCauses: FishboneSubCause[];\n /** Optional verbatim <title> override (defaults to the label). */\n title?: string;\n}\n\nexport interface FishboneCategory {\n id: number;\n label: string;\n causes: FishboneCause[];\n /** Optional verbatim <title> override (defaults to the label). */\n title?: string;\n}\n\nexport interface FishboneInput {\n /** The effect, written in the head box (verbatim). */\n effectLabel: string;\n /** Declared order is honored: first category nearest the head, alternating top/bottom. */\n categories: FishboneCategory[];\n}\n\n// ── Display vocabulary ────────────────────────────────────────────────────────\n\nexport interface FishboneLabels {\n /** Legend label for the cause-twig stroke (emitted only when sub-causes exist). */\n cause: string;\n /** Legend label for the sub-cause-twig stroke. */\n subCause: string;\n ariaLabel: string;\n}\n\nexport const FISHBONE_LABELS_EN: FishboneLabels = {\n cause: \"Cause\",\n subCause: \"Sub-cause\",\n ariaLabel: \"Cause-and-effect diagram (Ishikawa)\",\n};\n\n// ── Validation ────────────────────────────────────────────────────────────────\n\nexport type FishboneValidationCode = \"duplicate-id\";\n\nexport interface FishboneValidationIssue {\n /** Stable, machine-readable kebab-case code. */\n code: FishboneValidationCode;\n message: string;\n}\n\n/**\n * Thrown when ids collide within a namespace (categories / causes / sub-causes) —\n * the decoration hooks would be ambiguous. Carries ALL issues, deterministically\n * sorted (namespace order, then ascending id), never just the first. Everything\n * else (empty labels, zero causes, zero categories) is tolerated and drawn as\n * declared. The message mirrors the fault-tree shape (`invalid fishbone: …; …`)\n * so message-only logging keeps the diagram context for both modules.\n */\nexport class FishboneValidationError extends Error {\n readonly issues: readonly FishboneValidationIssue[];\n\n constructor(issues: readonly FishboneValidationIssue[]) {\n super(`invalid fishbone: ${issues.map((i) => i.message).join(\"; \")}`);\n this.name = \"FishboneValidationError\";\n this.issues = issues;\n }\n}\n\nfunction duplicateIds(ids: readonly number[]): number[] {\n const seen = new Set<number>();\n const dups = new Set<number>();\n for (const id of ids) {\n if (seen.has(id)) dups.add(id);\n seen.add(id);\n }\n return [...dups].sort((a, b) => a - b);\n}\n\nfunction validateIds(input: FishboneInput): void {\n const issues: FishboneValidationIssue[] = [];\n const collect = (noun: string, ids: readonly number[]): void => {\n for (const id of duplicateIds(ids)) {\n issues.push({ code: \"duplicate-id\", message: `duplicate ${noun} id ${id}` });\n }\n };\n collect(\"category\", input.categories.map((c) => c.id));\n collect(\"cause\", input.categories.flatMap((c) => c.causes.map((k) => k.id)));\n collect(\n \"sub-cause\",\n input.categories.flatMap((c) => c.causes.flatMap((k) => k.subCauses.map((s) => s.id))),\n );\n if (issues.length > 0) throw new FishboneValidationError(issues);\n}\n\n// ── Options ───────────────────────────────────────────────────────────────────\n\nexport interface FishboneSvgOptions {\n /** Display clamp per label (verbatim text stays in the <title>). */\n maxLabelChars?: number;\n /** Label font size (px); default 12. */\n fontSize?: number;\n /** Set false to suppress the legend; default true (emits only when sub-causes exist). */\n legend?: boolean;\n /** Arrowheads on spine/bones/twigs (classic Ishikawa); default true. */\n arrowheads?: boolean;\n /** Display vocabulary — English default; see `compasso/locales/pt-br`. */\n labels?: FishboneLabels;\n}\n\n// ── Geometry constants ────────────────────────────────────────────────────────\n\n// Hard-coded trig literals for the 60° bone angle (see module header).\nconst TAN60 = 1.7320508075688772;\nconst SIN60 = 0.8660254037844386;\nconst COS60 = 0.5;\n\nconst PADDING = 32;\nconst ROW_GAP = 12;\n/** Min horizontal air between adjacent same-side bone content AABBs. */\nconst BONE_GAP = 36;\nconst CAT_PAD_X = 12;\nconst CAT_PAD_Y = 8;\n/** Air between the bone's outer tip and the category box. */\nconst CAT_GAP = 12;\nconst TAIL_EXTRA = 44;\nconst HEAD_GAP = 24;\nconst HEAD_PAD_X = 14;\nconst HEAD_PAD_Y = 10;\n/** Sub-cause labels are ONE clamped line at this per-line budget (band invariant). */\nconst SUB_PER_LINE = 18;\n/** Helvetica-like glyph box at the 12px reference: ascent 11 above the baseline,\n * descent 3 below. EVERY fontSize-dependent vertical reserve is derived from these\n * (see verticalMetrics) — never used directly, so no 12px-only constant can leak\n * into the layout. */\nconst ASCENT_12 = 11;\nconst DESCENT_12 = 3;\n\n// ── fontSize-derived vertical metrics ─────────────────────────────────────────\n\ninterface VerticalMetrics {\n /** Baseline-to-glyph-top reserve (mirrors outward-stacked text on bottom bones). */\n ascent: number;\n /** Stacked-line pitch: EXACTLY ascent + descent, so consecutive baselines one\n * lineH apart give glyph bands that touch without overlapping, at any fontSize. */\n lineH: number;\n /** Innermost baseline offset from its twig: descent + 1 keeps the glyph ink 1px\n * clear of the twig stroke at any fontSize (4 at the default 12). */\n twigGap: number;\n /** Sub-twig vertical rise. blockH ≤ 2·lineH = sV − twigGap − 8 < sV, so within one\n * band the cause-label strip and the sub-label strip occupy disjoint y-slabs with\n * ≥ 8px air, and a sub-twig stays spine-ward of its label's center while crossing\n * the cause strip (both by construction, at any fontSize). */\n sV: number;\n /** First cause band starts |y| ≥ spineClear from the spine (the cross-side slab);\n * tied to the 2-line strip height so the slab tracks the type scale — anything\n * beyond the fixed ~10px bone-arrowhead reach preserves the guarantee. */\n spineClear: number;\n /** Twig length beyond its content: the bone's horizontal run across a 2-line cause\n * strip plus 25px of air — strictly above the (2·lineH + twigGap)/TAN60 + 10\n * floor the label-vs-bone clearance needs, by construction at any fontSize. */\n boneClear: number;\n /** Min horizontal air between adjacent ×1.2-padded sub-label boxes on one twig\n * (one line-height, so the air scales with the type; see the station packing). */\n subGap: number;\n}\n\n/** Derives the vertical reserves from the glyph metrics at the requested size, so\n * the slab/clearance inequalities in fishboneSvg hold BY CONSTRUCTION at any\n * fontSize. At the default 12 they reduce exactly to the original constants\n * (lineH 14, twigGap 4, sV 40, spineClear 32, boneClear 44, subGap 14), keeping\n * default output byte-stable (pinned by test/fishbone/render.test.ts). */\nfunction verticalMetrics(fontSize: number): VerticalMetrics {\n // Ceil-scaled so a reserve never rounds below the true em-scaled glyph extent.\n const ascent = Math.ceil((ASCENT_12 * fontSize) / 12);\n const descent = Math.ceil((DESCENT_12 * fontSize) / 12);\n const lineH = ascent + descent;\n const twigGap = descent + 1;\n return {\n ascent,\n lineH,\n twigGap,\n sV: 2 * lineH + twigGap + 8,\n spineClear: 2 * lineH + twigGap,\n boneClear: Math.ceil((2 * lineH + twigGap) / TAN60) + 25,\n subGap: lineH,\n };\n}\n\n// Ink (zinc ramp on white — matches the genogram/ecomap emitters). The four line\n// levels carry a decreasing stroke hierarchy (the only styled distinction a reader\n// could miss — exactly what the conditional legend names).\nconst EDGE_INK = \"#71717a\";\nconst BOX_STROKE = \"#52525b\";\nconst LABEL_FILL = \"#3f3f46\";\nconst SPINE_W = 2.5;\nconst SPINE_OP = 0.85;\nconst BONE_W = 2;\nconst BONE_OP = 0.8;\nconst TWIG_W = 1.5;\nconst TWIG_OP = 0.75;\nconst SUB_W = 1.2;\nconst SUB_OP = 0.7;\n\nconst round = (n: number): number => Math.round(n * 100) / 100;\n\n/** Arrowhead polygon: tip at (tipX,tipY), pointing along the unit vector (ux,uy). */\nfunction arrowHead(tipX: number, tipY: number, ux: number, uy: number, opacity: number): string {\n const LEN = 9;\n const HALF_W = 4.5;\n const bx = tipX - ux * LEN;\n const by = tipY - uy * LEN;\n const px = -uy;\n const py = ux;\n const points = [\n `${round(tipX)},${round(tipY)}`,\n `${round(bx + px * HALF_W)},${round(by + py * HALF_W)}`,\n `${round(bx - px * HALF_W)},${round(by - py * HALF_W)}`,\n ].join(\" \");\n return `<polygon points=\"${points}\" fill=\"${EDGE_INK}\" fill-opacity=\"${opacity}\"/>`;\n}\n\nfunction lineEl(x1: number, y1: number, x2: number, y2: number, w: number, op: number): string {\n return `<line x1=\"${round(x1)}\" y1=\"${round(y1)}\" x2=\"${round(x2)}\" y2=\"${round(y2)}\" stroke=\"${EDGE_INK}\" stroke-width=\"${w}\" stroke-opacity=\"${op}\"/>`;\n}\n\n// ── Internal measured shapes ──────────────────────────────────────────────────\n// All x positions below are relative to the bone's spine attachment; all vertical\n// positions are MAGNITUDES (distance from the spine) — the bottom side mirrors y.\n\ninterface SubBand {\n sub: FishboneSubCause;\n line: string;\n w: number;\n /** Station x on the cause twig. */\n sx: number;\n /** Outer end x of the diagonal sub-twig (= the label's center x). */\n outerX: number;\n}\n\ninterface CauseBand {\n cause: FishboneCause;\n lines: string[];\n labelW: number;\n blockH: number;\n /** Twig magnitude from the spine (the band's spine-side edge). */\n offset: number;\n bandH: number;\n /** Bone crossing at the twig's magnitude (≤ 0). */\n bx: number;\n /** Twig free end (tail-ward). */\n freeEnd: number;\n subs: SubBand[];\n}\n\ninterface Bone {\n category: FishboneCategory;\n up: boolean;\n bands: CauseBand[];\n /** Bone vertical magnitude (attach at the spine, outer end at −B / +B). */\n B: number;\n catLines: string[];\n boxW: number;\n boxH: number;\n /** Content extents relative to the attach: [ax − relL, ax + relR] contains ALL ink. */\n relL: number;\n relR: number;\n /** Absolute attach x (assigned by per-side packing). */\n ax: number;\n}\n\n/**\n * Renders a declared Ishikawa diagram to a self-contained SVG string. Deterministic:\n * same data → same SVG. Zero categories yield a valid spine+head-only SVG. Throws\n * FishboneValidationError on duplicate ids within a namespace (all issues listed).\n * The root keeps numeric width/height attributes plus a matching viewBox.\n *\n * Collision guarantee, by construction from MEASURED label widths (asserted by\n * test/fishbone/geometry.test.ts, including at non-default font sizes). Every\n * vertical reserve is derived from the glyph metrics at opts.fontSize (see\n * verticalMetrics), so the inequalities hold at ANY fontSize, not only the default:\n * 1. within a band: the cause-label strip and the sub-label strip occupy disjoint\n * y-slabs (blockH ≤ 2·lineH = sV − twigGap − 8 < sV); sub labels are x-disjoint\n * by station packing over ×1.2-padded estimates (absorbs the estimator's\n * Helvetica-caps deficit); both strips clear the slanted bone by the\n * station/boneClear inequalities;\n * 2. across bands of one bone: bands are stacked disjoint y-intervals of measured\n * heights (ROW_GAP included), and the bone crosses each slab only at its own\n * clearance-checked x;\n * 3. across bones of one side: disjoint content AABBs packed with BONE_GAP;\n * 4. across sides: open half-planes separated by the |y| < spineClear spine slab;\n * 5. head/tail: the head box sits beyond every bone's right extent + HEAD_GAP.\n * Every reserved width comes from estimateTextWidth (deliberately wide, core\n * doctrine) and the emitter draws at the same font metrics — reserved ⊇ drawn.\n */\nexport function fishboneSvg(input: FishboneInput, opts: FishboneSvgOptions = {}): string {\n validateIds(input);\n\n const fontSize = opts.fontSize ?? 12;\n const { ascent, lineH, twigGap, sV, spineClear, boneClear, subGap } = verticalMetrics(fontSize);\n const labels = opts.labels ?? FISHBONE_LABELS_EN;\n const arrows = opts.arrowheads !== false;\n\n // ── Measure every bone (declared order; even index → top, odd → bottom). ─────\n const bones: Bone[] = input.categories.map((category, idx) => {\n let cursor = spineClear;\n const bands: CauseBand[] = category.causes.map((cause) => {\n const lines = wrapLabelBalanced(clampLabel(cause.label, opts.maxLabelChars));\n const labelW = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize)));\n const blockH = lines.length * lineH;\n const offset = cursor;\n const bx = -offset / TAN60;\n\n // Sub stations right→left in declared order. The first inequality keeps the\n // bone-nearest label clear of the slanted bone at the sub strip's outer edge\n // ((sV − twigGap)/TAN60 + 10 of air, which dwarfs the estimator's half-label\n // caps deficit at any fontSize). Successive stations keep the label boxes\n // disjoint with subGap air between ×1.2-PADDED estimates: CHAR_W 0.6 under-\n // reads Helvetica CAPS (≈ 0.72 em/char average), so padding each estimated\n // width by 20% restores reserved ⊇ drawn for caps-heavy labels — runs wider\n // than that (M/W walls) remain core-estimator doctrine, as everywhere else.\n const subs: SubBand[] = [];\n for (const sub of cause.subCauses) {\n const line = wrapLabel(clampLabel(sub.label, opts.maxLabelChars), SUB_PER_LINE, 1)[0]!;\n const w = estimateTextWidth(line, fontSize);\n const prev = subs.length > 0 ? subs[subs.length - 1]! : null;\n const sx =\n prev === null\n ? bx - (sV + lineH) / TAN60 - 10 - w / 2\n : prev.sx - (prev.w + w) * 0.6 - subGap;\n subs.push({ sub, line, w, sx, outerX: sx - sV / TAN60 });\n }\n\n const last = subs.length > 0 ? subs[subs.length - 1]! : null;\n const subSpan = last === null ? 0 : bx - (last.outerX - last.w / 2);\n // Twig length reserves, beyond the boneClear terms, a third term when subs\n // exist: the cause label must end BEFORE the leftmost sub-twig's outer x —\n // the diagonal sub-twigs descend through the cause-label y-slab, so a long\n // cause label hugging the free end could otherwise sit under them. (The\n // band-disjointness argument covers label strips, not the sub LINES.)\n const twigLen =\n last === null\n ? labelW + boneClear\n : Math.max(labelW + boneClear, subSpan + boneClear, labelW + (bx - last.outerX) + 10);\n\n const bandH = Math.max(blockH + twigGap, subs.length > 0 ? sV + lineH + twigGap : 0) + ROW_GAP;\n cursor += bandH;\n return { cause, lines, labelW, blockH, offset, bandH, bx, freeEnd: bx - twigLen, subs };\n });\n\n const B = cursor + 16;\n const catLines = wrapLabelBalanced(clampLabel(category.label, opts.maxLabelChars));\n const boxW = Math.max(...catLines.map((l) => estimateTextWidth(l, fontSize))) + CAT_PAD_X * 2;\n const boxH = catLines.length * lineH + CAT_PAD_Y * 2;\n const tipX = -B / TAN60;\n return {\n category,\n up: idx % 2 === 0,\n bands,\n B,\n catLines,\n boxW,\n boxH,\n // Left extent: the category box's left edge or the deepest twig free end —\n // every label sits AT or right of its twig's free end, every sub label sits\n // right of the free end too (twigLen ≥ subSpan + boneClear).\n relL: Math.max(B / TAN60 + boxW / 2, ...bands.map((b) => -b.freeEnd)),\n // Right extent: a wide category box on a short bone may overhang the attach.\n relR: Math.max(0, boxW / 2 + tipX),\n ax: 0,\n };\n });\n\n // ── Pack each side independently, right→left in declared order. ──────────────\n const cursors = { top: 0, bottom: 0 };\n for (const bone of bones) {\n const side = bone.up ? \"top\" : \"bottom\";\n bone.ax = cursors[side] - bone.relR;\n cursors[side] = bone.ax - bone.relL - BONE_GAP;\n }\n\n // ── Head box + spine extents (working coords: spine y = 0). ──────────────────\n const effLines = wrapLabelBalanced(clampLabel(input.effectLabel, opts.maxLabelChars));\n const headW = Math.max(...effLines.map((l) => estimateTextWidth(l, fontSize))) + HEAD_PAD_X * 2;\n const headH = effLines.length * lineH + HEAD_PAD_Y * 2;\n // Every bone's right extent is ≤ 0 by packing, so the head clears all of them.\n const headLeft = bones.length > 0 ? HEAD_GAP : 0;\n const tailX =\n bones.length > 0\n ? Math.min(...bones.map((b) => b.ax - b.relL)) - TAIL_EXTRA\n : headLeft - (TAIL_EXTRA + HEAD_GAP);\n\n // ── Bounds → shift positive (ecomap pattern). ────────────────────────────────\n let minY = -headH / 2;\n let maxY = headH / 2;\n for (const bone of bones) {\n const reach = bone.B + CAT_GAP + bone.boxH;\n if (bone.up) minY = Math.min(minY, -reach);\n else maxY = Math.max(maxY, reach);\n }\n const dx = PADDING - tailX;\n const dy = PADDING - minY;\n let width = headLeft + headW - tailX + PADDING * 2;\n let height = maxY - minY + PADDING * 2;\n const spineY = dy;\n\n const centeredYs = (cy: number, n: number): number[] =>\n Array.from({ length: n }, (_, i) => cy - ((n - 1) * lineH) / 2 + i * lineH + fontSize * 0.32);\n\n // Band text stacks OUTWARD from its strip's spine-side edge (magnitude e): on top\n // bones the innermost line's baseline sits twigGap beyond the edge (= descent + 1,\n // ink 1px clear of the twig at any fontSize); the bottom side mirrors with the\n // ascent so the glyphs occupy the mirrored strip.\n const bandBaselines = (e: number, n: number, up: boolean): number[] =>\n Array.from({ length: n }, (_, k) =>\n up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH,\n );\n\n const textBlock = (anchor: \"start\" | \"middle\", x: number, ys: number[], lines: string[]): string =>\n `<text text-anchor=\"${anchor}\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">` +\n lines.map((line, i) => `<tspan x=\"${round(x)}\" y=\"${round(ys[i]!)}\">${xmlEscape(line)}</tspan>`).join(\"\") +\n `</text>`;\n\n const parts: string[] = [];\n\n // ── Spine (under everything), arrow into the head's left edge. ───────────────\n {\n const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];\n if (arrows) body.push(arrowHead(headLeft + dx, spineY, 1, 0, SPINE_OP));\n parts.push(`<g data-edge-id=\"spine\"><title>${xmlEscape(labels.ariaLabel)}</title>${body.join(\"\")}</g>`);\n }\n\n // ── Bones, twigs, sub-twigs — declared order. ────────────────────────────────\n for (const bone of bones) {\n const sgn = bone.up ? -1 : 1;\n const ax = bone.ax + dx;\n const tipX = ax - bone.B / TAN60;\n\n const body = [lineEl(tipX, spineY + sgn * bone.B, ax, spineY, BONE_W, BONE_OP)];\n // The bone runs from the outer tip INTO the spine: on a top bone that direction\n // is down-right (+y), on a bottom bone up-right (−y) — opposite the side sign.\n if (arrows) body.push(arrowHead(ax, spineY, COS60, -sgn * SIN60, BONE_OP));\n const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;\n body.push(\n `<rect x=\"${round(tipX - bone.boxW / 2)}\" y=\"${round(boxTop)}\" width=\"${round(bone.boxW)}\" height=\"${round(bone.boxH)}\" rx=\"2\" fill=\"transparent\" stroke=\"${BOX_STROKE}\" stroke-width=\"1.5\"/>`,\n );\n body.push(textBlock(\"middle\", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));\n parts.push(\n `<g data-node-id=\"b${bone.category.id}\"><title>${xmlEscape(bone.category.title ?? bone.category.label)}</title>${body.join(\"\")}</g>`,\n );\n\n for (const band of bone.bands) {\n const ty = spineY + sgn * band.offset;\n const bx = ax + band.bx;\n const freeEnd = ax + band.freeEnd;\n const cbody = [lineEl(freeEnd, ty, bx, ty, TWIG_W, TWIG_OP)];\n if (arrows) cbody.push(arrowHead(bx, ty, 1, 0, TWIG_OP));\n cbody.push(textBlock(\"start\", freeEnd + 2, bandBaselines(band.offset, band.lines.length, bone.up), band.lines));\n parts.push(\n `<g data-node-id=\"c${band.cause.id}\"><title>${xmlEscape(band.cause.title ?? band.cause.label)}</title>${cbody.join(\"\")}</g>`,\n );\n\n for (const s of band.subs) {\n const sx = ax + s.sx;\n const ox = ax + s.outerX;\n const sbody = [lineEl(ox, spineY + sgn * (band.offset + sV), sx, ty, SUB_W, SUB_OP)];\n // Parallel to the bone, pointing into the twig — same spine-ward direction.\n if (arrows) sbody.push(arrowHead(sx, ty, COS60, -sgn * SIN60, SUB_OP));\n const baseline = bone.up\n ? spineY - (band.offset + sV + twigGap)\n : spineY + band.offset + sV + twigGap + ascent;\n sbody.push(textBlock(\"middle\", ox, [baseline], [s.line]));\n parts.push(`<g data-node-id=\"s${s.sub.id}\"><title>${xmlEscape(s.sub.label)}</title>${sbody.join(\"\")}</g>`);\n }\n }\n }\n\n // ── Effect head (on top), vertically centered on the spine. ──────────────────\n {\n const x = headLeft + dx;\n parts.push(\n `<g data-node-id=\"head\"><title>${xmlEscape(input.effectLabel)}</title>` +\n `<rect x=\"${round(x)}\" y=\"${round(spineY - headH / 2)}\" width=\"${round(headW)}\" height=\"${round(headH)}\" rx=\"2\" fill=\"transparent\" stroke=\"${BOX_STROKE}\" stroke-width=\"2\"/>` +\n textBlock(\"middle\", x + headW / 2, centeredYs(spineY, effLines.length), effLines) +\n `</g>`,\n );\n }\n\n // ── Conditional legend: the fishbone is fully labeled inline — the only styled\n // distinction a reader could miss is the cause vs sub-cause stroke hierarchy,\n // so used-keys-only means: both entries when at least one sub-cause exists,\n // otherwise no legend at all. ────────────────────────────────────────────────\n const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));\n if (opts.legend !== false && anySubs) {\n const entries: LegendEntry[] = [\n {\n swatch: (x, y) => lineEl(x, y, x + LEGEND_SWATCH_W, y, TWIG_W, TWIG_OP),\n label: labels.cause,\n },\n {\n swatch: (x, y) => lineEl(x, y + 5, x + LEGEND_SWATCH_W, y - 5, SUB_W, SUB_OP),\n label: labels.subCause,\n },\n ];\n const block = legendBlock(entries, height);\n parts.push(block.svg);\n width = Math.max(width, block.width);\n height = block.height;\n }\n\n const w = Math.ceil(width);\n const h = Math.ceil(height);\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${w} ${h}\" width=\"${w}\" height=\"${h}\" role=\"img\" aria-label=\"${xmlEscape(labels.ariaLabel)}\">` +\n parts.join(\"\") +\n `</svg>`\n );\n}\n"]}
|
package/dist/core/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(" ");
|
|
@@ -15,19 +44,27 @@ var CHAR_W = 0.6;
|
|
|
15
44
|
function estimateTextWidth(text, fontPx) {
|
|
16
45
|
return text.length * fontPx * CHAR_W;
|
|
17
46
|
}
|
|
18
|
-
function wrapLabel(label, perLine) {
|
|
47
|
+
function wrapLabel(label, perLine, maxLines = 2) {
|
|
19
48
|
if (label.length <= perLine) return [label];
|
|
20
49
|
const cap = (s) => s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + "\u2026" : s;
|
|
21
|
-
|
|
22
|
-
let line2 = "";
|
|
50
|
+
const lines = [""];
|
|
23
51
|
for (const word of label.split(/\s+/)) {
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
const last = lines.length - 1;
|
|
53
|
+
const current = lines[last];
|
|
54
|
+
if (current === "" || (current + " " + word).length <= perLine) {
|
|
55
|
+
lines[last] = current === "" ? word : `${current} ${word}`;
|
|
56
|
+
} else if (lines.length < maxLines) {
|
|
57
|
+
lines.push(word);
|
|
26
58
|
} else {
|
|
27
|
-
|
|
59
|
+
lines[last] = `${current} ${word}`;
|
|
28
60
|
}
|
|
29
61
|
}
|
|
30
|
-
|
|
62
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
63
|
+
return lines.map(cap);
|
|
64
|
+
}
|
|
65
|
+
function wrapLabelBalanced(label, maxLines) {
|
|
66
|
+
const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));
|
|
67
|
+
return wrapLabel(label, perLine, maxLines);
|
|
31
68
|
}
|
|
32
69
|
function clampLabel(label, maxChars) {
|
|
33
70
|
if (maxChars === void 0 || label.length <= maxChars) return label;
|
|
@@ -35,6 +72,28 @@ function clampLabel(label, maxChars) {
|
|
|
35
72
|
}
|
|
36
73
|
var FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
37
74
|
|
|
75
|
+
// src/core/legend.ts
|
|
76
|
+
var LEGEND_ROW_H = 18;
|
|
77
|
+
var LEGEND_PAD = 16;
|
|
78
|
+
var LEGEND_SWATCH_W = 22;
|
|
79
|
+
var LEGEND_GAP = 14;
|
|
80
|
+
var LEGEND_FONT = 11;
|
|
81
|
+
var LEGEND_TEXT_FILL = "#52525b";
|
|
82
|
+
function legendBlock(entries, startY) {
|
|
83
|
+
if (entries.length === 0) return { svg: "", width: 0, height: startY };
|
|
84
|
+
const rows = entries.map((entry, i) => {
|
|
85
|
+
const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;
|
|
86
|
+
const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;
|
|
87
|
+
return entry.swatch(LEGEND_PAD, rowCenterY) + `<text x="${textX}" y="${rowCenterY + LEGEND_FONT * 0.32}" font-family="${FONT_FAMILY}" font-size="${LEGEND_FONT}" fill="${LEGEND_TEXT_FILL}">${xmlEscape(entry.label)}</text>`;
|
|
88
|
+
});
|
|
89
|
+
const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);
|
|
90
|
+
return {
|
|
91
|
+
svg: `<g data-compasso-legend="true">${rows.join("")}</g>`,
|
|
92
|
+
width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,
|
|
93
|
+
height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
// src/core/stroke.ts
|
|
39
98
|
var EDGE_STROKE = {
|
|
40
99
|
plain: { width: 1.5, dash: null, opacity: 0.6 },
|
|
@@ -86,13 +145,21 @@ function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
|
|
|
86
145
|
exports.CHAR_W = CHAR_W;
|
|
87
146
|
exports.EDGE_STROKE = EDGE_STROKE;
|
|
88
147
|
exports.FONT_FAMILY = FONT_FAMILY;
|
|
148
|
+
exports.LEGEND_FONT = LEGEND_FONT;
|
|
149
|
+
exports.LEGEND_GAP = LEGEND_GAP;
|
|
150
|
+
exports.LEGEND_PAD = LEGEND_PAD;
|
|
151
|
+
exports.LEGEND_ROW_H = LEGEND_ROW_H;
|
|
152
|
+
exports.LEGEND_SWATCH_W = LEGEND_SWATCH_W;
|
|
89
153
|
exports.QUALITY_LEXICON_EN = QUALITY_LEXICON_EN;
|
|
90
154
|
exports.clampLabel = clampLabel;
|
|
91
155
|
exports.estimateTextWidth = estimateTextWidth;
|
|
156
|
+
exports.legendBlock = legendBlock;
|
|
92
157
|
exports.normalizeText = normalizeText;
|
|
93
158
|
exports.pathData = pathData;
|
|
94
159
|
exports.qualityLineStyle = qualityLineStyle;
|
|
160
|
+
exports.romanNumeral = romanNumeral;
|
|
95
161
|
exports.wrapLabel = wrapLabel;
|
|
162
|
+
exports.wrapLabelBalanced = wrapLabelBalanced;
|
|
96
163
|
exports.xmlEscape = xmlEscape;
|
|
97
164
|
//# sourceMappingURL=index.cjs.map
|
|
98
165
|
//# sourceMappingURL=index.cjs.map
|
package/dist/core/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/xml.ts","../../src/core/geometry.ts","../../src/core/text.ts","../../src/core/stroke.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// XML/SVG escaping — every interpolated text in an emitted SVG MUST pass through\n// this. Diagram labels are typically user/author-controlled, and the SVG string may\n// be injected via innerHTML in a browser or embedded as vectors in a PDF — an\n// unescaped label is an XSS / `</svg>`-breakout vector on every surface at once.\n\n/** Escapes a string for use in SVG/XML text content AND attribute values. */\nexport function xmlEscape(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","// Shared geometry primitives.\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** \"M x y L x y …\" path data from a polyline. */\nexport function pathData(points: Point[]): string {\n return points.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`).join(\" \");\n}\n","// Pure, deterministic text metrics — no DOM, no canvas, no font files. Every layout\n// in this library reserves space from these estimates and every emitter draws with\n// the same constants, so what is proven collision-free is what is drawn.\n\n/**\n * Conservative per-character advance, as a fraction of the font size. The pure module\n * can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *\n * CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a\n * layout reserves slightly more room than the text needs, so the real render always\n * fits inside its reserved box.\n */\nexport const CHAR_W = 0.6;\n\n/** Pure, deterministic width estimate for a single line of text at `fontPx`. */\nexport function estimateTextWidth(text: string, fontPx: number): number {\n return text.length * fontPx * CHAR_W;\n}\n\n/**\n * Wraps a label onto up to TWO lines so long labels don't sprawl. Greedy word fill;\n * each line capped at `perLine` chars (…-truncated only if a single word overflows).\n * Pure + shared so every renderer wraps identically. The full text is always kept\n * elsewhere (the node's `label`, an SVG <title>, or a side list), so nothing is\n * silently lost.\n */\nexport function wrapLabel(label: string, perLine: number): string[] {\n if (label.length <= perLine) return [label];\n const cap = (s: string): string =>\n s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + \"…\" : s;\n let line1 = \"\";\n let line2 = \"\";\n for (const word of label.split(/\\s+/)) {\n if (line2 === \"\" && (line1 === \"\" || (line1 + \" \" + word).length <= perLine)) {\n line1 = line1 === \"\" ? word : `${line1} ${word}`;\n } else {\n line2 = line2 === \"\" ? word : `${line2} ${word}`;\n }\n }\n return line2 === \"\" ? [cap(line1)] : [cap(line1), cap(line2)];\n}\n\n/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */\nexport function clampLabel(label: string, maxChars: number | undefined): string {\n if (maxChars === undefined || label.length <= maxChars) return label;\n return label.slice(0, Math.max(1, maxChars - 1)) + \"…\";\n}\n\n/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */\nexport const FONT_FAMILY = \"Helvetica, Arial, sans-serif\";\n","// Edge line styles — a neutral, presentation-only style for a relationship line.\n// NOT a judgment: a deterministic, lexical hint derived from the author's own quality\n// word. The literal word always rides the element's <title>, so a style never replaces\n// what was said. Unknown/ambiguous quality → \"plain\".\n\nexport type EdgeLineStyle = \"plain\" | \"close\" | \"distant\" | \"conflict\" | \"cutoff\";\n\n/** Stroke attributes per style — shared by every renderer so SVG and PDF match. */\nexport interface EdgeStroke {\n width: number;\n /** SVG dash array / PDF dash pattern as [dash, gap]; null = solid. */\n dash: [number, number] | null;\n opacity: number;\n}\n\nexport const EDGE_STROKE: Record<EdgeLineStyle, EdgeStroke> = {\n plain: { width: 1.5, dash: null, opacity: 0.6 },\n close: { width: 3, dash: null, opacity: 0.85 },\n distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },\n conflict: { width: 2, dash: [2, 2], opacity: 0.75 },\n cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 },\n};\n\n/**\n * A pluggable lexicon mapping free-text quality words onto line styles. Needles are\n * diacritic-free, lowercase substrings of the author's own words; `negations` are\n * whole words that flip the meaning of the very word a needle would match (\"not\n * close\"), so their presence makes the matcher abstain to \"plain\" rather than risk\n * an inverted visual. Locale packs (e.g. `compasso/locales/pt-br`) ship alternates.\n */\nexport interface QualityLexicon {\n buckets: readonly { style: Exclude<EdgeLineStyle, \"plain\">; needles: readonly string[] }[];\n negations: readonly string[];\n}\n\n// English default. Conservative stems: each needle is a substring match on the\n// normalized text, and the SINGLE-BUCKET rule below keeps competing signals honest.\n// \"no\" alone is deliberately NOT a negation: \"no contact\" is a genuine cutoff signal.\nexport const QUALITY_LEXICON_EN: QualityLexicon = {\n buckets: [\n {\n style: \"close\",\n needles: [\"close\", \"warm\", \"support\", \"lov\", \"affection\", \"caring\", \"tight\", \"harmon\", \"healthy\"],\n },\n {\n style: \"distant\",\n needles: [\"distant\", \"detach\", \"absent\", \"cold\", \"drift\"],\n },\n {\n style: \"conflict\",\n needles: [\"conflict\", \"fight\", \"tens\", \"difficult\", \"hostil\", \"violen\", \"abus\", \"aggress\", \"complicat\", \"toxic\", \"argu\"],\n },\n {\n style: \"cutoff\",\n needles: [\"estrang\", \"cut off\", \"cutoff\", \"no contact\", \"broken off\", \"sever\"],\n },\n ],\n negations: [\"not\", \"never\", \"no longer\", \"hardly\"],\n};\n\n/** Lowercase + strip diacritics so matching ignores accents. */\nexport function normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\").toLowerCase();\n}\n\nconst escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Maps a free-text relationship quality to a neutral line style. Deterministic and\n * conservative: returns a specific style ONLY when exactly one lexical bucket matches;\n * null/empty/ambiguous/negated/unknown all fall back to \"plain\". The caller still\n * keeps the verbatim quality word, so nothing the author said is ever lost.\n */\nexport function qualityLineStyle(\n quality: string | null,\n lexicon: QualityLexicon = QUALITY_LEXICON_EN,\n): EdgeLineStyle {\n if (quality === null) return \"plain\";\n const haystack = normalizeText(quality);\n if (haystack.trim() === \"\") return \"plain\";\n\n if (lexicon.negations.length > 0) {\n const negation = new RegExp(`\\\\b(${lexicon.negations.map(escapeRegExp).join(\"|\")})\\\\b`);\n if (negation.test(haystack)) return \"plain\";\n }\n\n const matched: EdgeLineStyle[] = [];\n for (const { style, needles } of lexicon.buckets) {\n if (needles.some((n) => haystack.includes(n))) matched.push(style);\n }\n return matched.length === 1 ? matched[0]! : \"plain\";\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/xml.ts","../../src/core/roman.ts","../../src/core/geometry.ts","../../src/core/text.ts","../../src/core/legend.ts","../../src/core/stroke.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// XML/SVG escaping — every interpolated text in an emitted SVG MUST pass through\n// this. Diagram labels are typically user/author-controlled, and the SVG string may\n// be injected via innerHTML in a browser or embedded as vectors in a PDF — an\n// unescaped label is an XSS / `</svg>`-breakout vector on every surface at once.\n\n/** Escapes a string for use in SVG/XML text content AND attribute values. */\nexport function xmlEscape(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","// Roman numeral formatting for level/generation labels (pedigree generation rows\n// I, II, III…; any leveled diagram). Standard subtractive notation, valid for\n// integers 1..3999. A renderer must NEVER crash on a weird generation index, so\n// out-of-range / non-integer inputs fall back to the Arabic number as a string\n// rather than throwing or emitting nonsense like \"MMMM…\". Pure + deterministic.\n\nconst ROMAN_TABLE: ReadonlyArray<readonly [number, string]> = [\n [1000, \"M\"],\n [900, \"CM\"],\n [500, \"D\"],\n [400, \"CD\"],\n [100, \"C\"],\n [90, \"XC\"],\n [50, \"L\"],\n [40, \"XL\"],\n [10, \"X\"],\n [9, \"IX\"],\n [5, \"V\"],\n [4, \"IV\"],\n [1, \"I\"],\n];\n\n/** Uppercase Roman numeral for an integer in 1..3999 (1→\"I\", 4→\"IV\", 1990→\"MCMXC\").\n * Out of range or non-integer → the Arabic number as a string (graceful fallback). */\nexport function romanNumeral(n: number): string {\n if (!Number.isInteger(n) || n < 1 || n > 3999) return String(n);\n let remaining = n;\n let out = \"\";\n for (const [value, symbol] of ROMAN_TABLE) {\n while (remaining >= value) {\n out += symbol;\n remaining -= value;\n }\n }\n return out;\n}\n","// Shared geometry primitives.\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** \"M x y L x y …\" path data from a polyline. */\nexport function pathData(points: Point[]): string {\n return points.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x} ${p.y}`).join(\" \");\n}\n","// Pure, deterministic text metrics — no DOM, no canvas, no font files. Every layout\n// in this library reserves space from these estimates and every emitter draws with\n// the same constants, so what is proven collision-free is what is drawn.\n\n/**\n * Conservative per-character advance, as a fraction of the font size. The pure module\n * can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *\n * CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a\n * layout reserves slightly more room than the text needs, so the real render always\n * fits inside its reserved box.\n */\nexport const CHAR_W = 0.6;\n\n/** Pure, deterministic width estimate for a single line of text at `fontPx`. */\nexport function estimateTextWidth(text: string, fontPx: number): number {\n return text.length * fontPx * CHAR_W;\n}\n\n/**\n * Wraps a label onto up to `maxLines` lines (default 2) so long labels don't sprawl.\n * Greedy word fill; each line capped at `perLine` chars (…-truncated only if a line\n * still overflows). The LAST allowed line absorbs every remaining word before the cap\n * applies — words are never dropped mid-label, only visibly elided. Pure + shared so\n * every renderer wraps identically. The full text is always kept elsewhere (the\n * node's `label`, an SVG <title>, or a side list), so nothing is silently lost.\n *\n * INVARIANT: with the default `maxLines` the output is byte-identical to the\n * historical two-line implementation (pinned by test/core/text.test.ts) — both\n * shipped renderers wrap through here.\n */\nexport function wrapLabel(label: string, perLine: number, maxLines = 2): string[] {\n if (label.length <= perLine) return [label];\n const cap = (s: string): string =>\n s.length > perLine ? s.slice(0, Math.max(1, perLine - 1)) + \"…\" : s;\n const lines: string[] = [\"\"];\n for (const word of label.split(/\\s+/)) {\n const last = lines.length - 1;\n const current = lines[last]!;\n if (current === \"\" || (current + \" \" + word).length <= perLine) {\n lines[last] = current === \"\" ? word : `${current} ${word}`;\n } else if (lines.length < maxLines) {\n lines.push(word);\n } else {\n lines[last] = `${current} ${word}`;\n }\n }\n // A trailing-whitespace \"word\" (\"a b \".split(/\\s+/) → [\"a\",\"b\",\"\"]) may open a line\n // it never fills; the historical code dropped that empty line, so this does too.\n if (lines.length > 1 && lines[lines.length - 1] === \"\") lines.pop();\n return lines.map(cap);\n}\n\n/**\n * House \"balanced\" wrap policy (extracted verbatim from the ecomap's tie-label wrap,\n * itself mirroring the genogram's): per-line budget `min(26, max(14, ceil(len/2)+2))`\n * splits a medium label into two roughly even lines instead of one long + one short,\n * then the greedy `wrapLabel` fill (and its `maxLines` semantics) applies. The shipped\n * renderers keep their local copies for now — new modules consume this one; migrating\n * them is a separate provably-zero-change refactor.\n */\nexport function wrapLabelBalanced(label: string, maxLines?: number): string[] {\n const perLine = Math.min(26, Math.max(14, Math.ceil(label.length / 2) + 2));\n return wrapLabel(label, perLine, maxLines);\n}\n\n/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */\nexport function clampLabel(label: string, maxChars: number | undefined): string {\n if (maxChars === undefined || label.length <= maxChars) return label;\n return label.slice(0, Math.max(1, maxChars - 1)) + \"…\";\n}\n\n/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */\nexport const FONT_FAMILY = \"Helvetica, Arial, sans-serif\";\n","// Shared legend machinery — the exact row format the genogram and ecomap emitters\n// currently duplicate character-for-character (`<g data-compasso-legend=\"true\">` with\n// swatch-closure rows). Extracted so new diagram modules don't add a third copy;\n// the two shipped emitters keep their local blocks for now (migrating them is a\n// separate, provably-zero-change refactor — pinned byte-for-byte against the ecomap's\n// emitted legend in test/core/legend.test.ts).\n//\n// The caller stays in charge of the HONESTY RULE half: it decides WHICH entries exist\n// (used-keys-only) and what each swatch draws; this module only owns the row geometry,\n// the text emission (xmlEscape — labels may come from caller-supplied packs), and the\n// width/height bookkeeping both renderers compute identically.\n\nimport { FONT_FAMILY, estimateTextWidth } from \"./text\";\nimport { xmlEscape } from \"./xml\";\n\n// Row metrics shared by every legend in the library. Exported because swatch builders\n// need them (a line swatch spans LEGEND_SWATCH_W; a glyph swatch centers on it).\nexport const LEGEND_ROW_H = 18;\nexport const LEGEND_PAD = 16;\nexport const LEGEND_SWATCH_W = 22;\nexport const LEGEND_GAP = 14;\nexport const LEGEND_FONT = 11;\n\n// Legend text ink — the zinc glyph stroke both shipped emitters use for legend labels.\nconst LEGEND_TEXT_FILL = \"#52525b\";\n\nexport interface LegendEntry {\n /**\n * Swatch markup builder, called with the swatch's left x and the row's center y.\n * The builder owns its own escaping/rounding (it typically reuses the module's\n * glyph/line emitters); it should draw within LEGEND_SWATCH_W of x.\n */\n swatch: (x: number, yCenter: number) => string;\n /** Display label (escaped here; verbatim pack text in, escaped SVG out). */\n label: string;\n}\n\nexport interface LegendBlock {\n /** `<g data-compasso-legend=\"true\">…</g>` — empty string when there are no entries. */\n svg: string;\n /** Minimum canvas width the legend needs; callers take `max(width, block.width)`. */\n width: number;\n /** New total canvas height including the legend; callers assign it directly. */\n height: number;\n}\n\n/**\n * Emits the legend group for the given entries below `startY` (the diagram's current\n * height). Pure + deterministic; coordinates are interpolated exactly as the shipped\n * emitters do. Zero entries (used-keys-only found nothing) → no markup, zero width\n * contribution, height unchanged — safe to call unconditionally.\n */\nexport function legendBlock(entries: readonly LegendEntry[], startY: number): LegendBlock {\n if (entries.length === 0) return { svg: \"\", width: 0, height: startY };\n const rows = entries.map((entry, i) => {\n const rowCenterY = startY + i * LEGEND_ROW_H + LEGEND_ROW_H / 2;\n const textX = LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP;\n return (\n entry.swatch(LEGEND_PAD, rowCenterY) +\n `<text x=\"${textX}\" y=\"${rowCenterY + LEGEND_FONT * 0.32}\" font-family=\"${FONT_FAMILY}\" font-size=\"${LEGEND_FONT}\" fill=\"${LEGEND_TEXT_FILL}\">${xmlEscape(entry.label)}</text>`\n );\n });\n const widestLabel = entries.reduce((m, e) => Math.max(m, estimateTextWidth(e.label, LEGEND_FONT)), 0);\n return {\n svg: `<g data-compasso-legend=\"true\">${rows.join(\"\")}</g>`,\n width: LEGEND_PAD + LEGEND_SWATCH_W + LEGEND_GAP + widestLabel + LEGEND_PAD,\n height: startY + entries.length * LEGEND_ROW_H + LEGEND_PAD / 2,\n };\n}\n","// Edge line styles — a neutral, presentation-only style for a relationship line.\n// NOT a judgment: a deterministic, lexical hint derived from the author's own quality\n// word. The literal word always rides the element's <title>, so a style never replaces\n// what was said. Unknown/ambiguous quality → \"plain\".\n\nexport type EdgeLineStyle = \"plain\" | \"close\" | \"distant\" | \"conflict\" | \"cutoff\";\n\n/** Stroke attributes per style — shared by every renderer so SVG and PDF match. */\nexport interface EdgeStroke {\n width: number;\n /** SVG dash array / PDF dash pattern as [dash, gap]; null = solid. */\n dash: [number, number] | null;\n opacity: number;\n}\n\nexport const EDGE_STROKE: Record<EdgeLineStyle, EdgeStroke> = {\n plain: { width: 1.5, dash: null, opacity: 0.6 },\n close: { width: 3, dash: null, opacity: 0.85 },\n distant: { width: 1.5, dash: [4, 4], opacity: 0.55 },\n conflict: { width: 2, dash: [2, 2], opacity: 0.75 },\n cutoff: { width: 1.5, dash: [6, 5], opacity: 0.4 },\n};\n\n/**\n * A pluggable lexicon mapping free-text quality words onto line styles. Needles are\n * diacritic-free, lowercase substrings of the author's own words; `negations` are\n * whole words that flip the meaning of the very word a needle would match (\"not\n * close\"), so their presence makes the matcher abstain to \"plain\" rather than risk\n * an inverted visual. Locale packs (e.g. `compasso/locales/pt-br`) ship alternates.\n */\nexport interface QualityLexicon {\n buckets: readonly { style: Exclude<EdgeLineStyle, \"plain\">; needles: readonly string[] }[];\n negations: readonly string[];\n}\n\n// English default. Conservative stems: each needle is a substring match on the\n// normalized text, and the SINGLE-BUCKET rule below keeps competing signals honest.\n// \"no\" alone is deliberately NOT a negation: \"no contact\" is a genuine cutoff signal.\nexport const QUALITY_LEXICON_EN: QualityLexicon = {\n buckets: [\n {\n style: \"close\",\n needles: [\"close\", \"warm\", \"support\", \"lov\", \"affection\", \"caring\", \"tight\", \"harmon\", \"healthy\"],\n },\n {\n style: \"distant\",\n needles: [\"distant\", \"detach\", \"absent\", \"cold\", \"drift\"],\n },\n {\n style: \"conflict\",\n needles: [\"conflict\", \"fight\", \"tens\", \"difficult\", \"hostil\", \"violen\", \"abus\", \"aggress\", \"complicat\", \"toxic\", \"argu\"],\n },\n {\n style: \"cutoff\",\n needles: [\"estrang\", \"cut off\", \"cutoff\", \"no contact\", \"broken off\", \"sever\"],\n },\n ],\n negations: [\"not\", \"never\", \"no longer\", \"hardly\"],\n};\n\n/** Lowercase + strip diacritics so matching ignores accents. */\nexport function normalizeText(text: string): string {\n return text.normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\").toLowerCase();\n}\n\nconst escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * Maps a free-text relationship quality to a neutral line style. Deterministic and\n * conservative: returns a specific style ONLY when exactly one lexical bucket matches;\n * null/empty/ambiguous/negated/unknown all fall back to \"plain\". The caller still\n * keeps the verbatim quality word, so nothing the author said is ever lost.\n */\nexport function qualityLineStyle(\n quality: string | null,\n lexicon: QualityLexicon = QUALITY_LEXICON_EN,\n): EdgeLineStyle {\n if (quality === null) return \"plain\";\n const haystack = normalizeText(quality);\n if (haystack.trim() === \"\") return \"plain\";\n\n if (lexicon.negations.length > 0) {\n const negation = new RegExp(`\\\\b(${lexicon.negations.map(escapeRegExp).join(\"|\")})\\\\b`);\n if (negation.test(haystack)) return \"plain\";\n }\n\n const matched: EdgeLineStyle[] = [];\n for (const { style, needles } of lexicon.buckets) {\n if (needles.some((n) => haystack.includes(n))) matched.push(style);\n }\n return matched.length === 1 ? matched[0]! : \"plain\";\n}\n"]}
|
package/dist/core/index.d.cts
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
+
export { x as xmlEscape } from '../xml-DDae1eUr.cjs';
|
|
2
|
+
export { C as CHAR_W, F as FONT_FAMILY, P as Point, c as clampLabel, e as estimateTextWidth, p as pathData, w as wrapLabel, a as wrapLabelBalanced } from '../text-DuO_PwYw.cjs';
|
|
1
3
|
export { E as EDGE_STROKE, a as EdgeLineStyle, b as EdgeStroke, Q as QUALITY_LEXICON_EN, c as QualityLexicon, n as normalizeText, q as qualityLineStyle } from '../stroke-MQ427drt.cjs';
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
+
/** Uppercase Roman numeral for an integer in 1..3999 (1→"I", 4→"IV", 1990→"MCMXC").
|
|
6
|
+
* Out of range or non-integer → the Arabic number as a string (graceful fallback). */
|
|
7
|
+
declare function romanNumeral(n: number): string;
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
declare const LEGEND_ROW_H = 18;
|
|
10
|
+
declare const LEGEND_PAD = 16;
|
|
11
|
+
declare const LEGEND_SWATCH_W = 22;
|
|
12
|
+
declare const LEGEND_GAP = 14;
|
|
13
|
+
declare const LEGEND_FONT = 11;
|
|
14
|
+
interface LegendEntry {
|
|
15
|
+
/**
|
|
16
|
+
* Swatch markup builder, called with the swatch's left x and the row's center y.
|
|
17
|
+
* The builder owns its own escaping/rounding (it typically reuses the module's
|
|
18
|
+
* glyph/line emitters); it should draw within LEGEND_SWATCH_W of x.
|
|
19
|
+
*/
|
|
20
|
+
swatch: (x: number, yCenter: number) => string;
|
|
21
|
+
/** Display label (escaped here; verbatim pack text in, escaped SVG out). */
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
interface LegendBlock {
|
|
25
|
+
/** `<g data-compasso-legend="true">…</g>` — empty string when there are no entries. */
|
|
26
|
+
svg: string;
|
|
27
|
+
/** Minimum canvas width the legend needs; callers take `max(width, block.width)`. */
|
|
28
|
+
width: number;
|
|
29
|
+
/** New total canvas height including the legend; callers assign it directly. */
|
|
30
|
+
height: number;
|
|
9
31
|
}
|
|
10
|
-
/** "M x y L x y …" path data from a polyline. */
|
|
11
|
-
declare function pathData(points: Point[]): string;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Conservative per-character advance, as a fraction of the font size. The pure module
|
|
15
|
-
* can't measure real glyphs (no DOM/canvas), so it estimates width = chars * font *
|
|
16
|
-
* CHAR_W. It is deliberately a touch WIDER than Helvetica's ~0.5 average advance: a
|
|
17
|
-
* layout reserves slightly more room than the text needs, so the real render always
|
|
18
|
-
* fits inside its reserved box.
|
|
19
|
-
*/
|
|
20
|
-
declare const CHAR_W = 0.6;
|
|
21
|
-
/** Pure, deterministic width estimate for a single line of text at `fontPx`. */
|
|
22
|
-
declare function estimateTextWidth(text: string, fontPx: number): number;
|
|
23
32
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* silently lost.
|
|
33
|
+
* Emits the legend group for the given entries below `startY` (the diagram's current
|
|
34
|
+
* height). Pure + deterministic; coordinates are interpolated exactly as the shipped
|
|
35
|
+
* emitters do. Zero entries (used-keys-only found nothing) → no markup, zero width
|
|
36
|
+
* contribution, height unchanged — safe to call unconditionally.
|
|
29
37
|
*/
|
|
30
|
-
declare function
|
|
31
|
-
/** Caps a verbatim label for a COMPACT render (preview); full text kept by the caller. */
|
|
32
|
-
declare function clampLabel(label: string, maxChars: number | undefined): string;
|
|
33
|
-
/** Font stack shared by every emitter; PDF embedders typically map it onto Helvetica. */
|
|
34
|
-
declare const FONT_FAMILY = "Helvetica, Arial, sans-serif";
|
|
38
|
+
declare function legendBlock(entries: readonly LegendEntry[], startY: number): LegendBlock;
|
|
35
39
|
|
|
36
|
-
export {
|
|
40
|
+
export { LEGEND_FONT, LEGEND_GAP, LEGEND_PAD, LEGEND_ROW_H, LEGEND_SWATCH_W, type LegendBlock, type LegendEntry, legendBlock, romanNumeral };
|