compasso 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +86 -5
  2. package/dist/{chunk-WJYYBGZW.js → chunk-2NDET6O5.js} +3 -3
  3. package/dist/{chunk-WJYYBGZW.js.map → chunk-2NDET6O5.js.map} +1 -1
  4. package/dist/{chunk-LR7BXUWM.js → chunk-3RGYLVTN.js} +3 -3
  5. package/dist/{chunk-LR7BXUWM.js.map → chunk-3RGYLVTN.js.map} +1 -1
  6. package/dist/chunk-BM7UJBK5.js +680 -0
  7. package/dist/chunk-BM7UJBK5.js.map +1 -0
  8. package/dist/chunk-DVLWT565.js +372 -0
  9. package/dist/chunk-DVLWT565.js.map +1 -0
  10. package/dist/{chunk-IPE7JZO5.js → chunk-JBDA7E2O.js} +3 -3
  11. package/dist/{chunk-IPE7JZO5.js.map → chunk-JBDA7E2O.js.map} +1 -1
  12. package/dist/chunk-MIJTBYX2.js +982 -0
  13. package/dist/chunk-MIJTBYX2.js.map +1 -0
  14. package/dist/{chunk-PGUMLTIM.js → chunk-PJHLWSGD.js} +3 -3
  15. package/dist/{chunk-PGUMLTIM.js.map → chunk-PJHLWSGD.js.map} +1 -1
  16. package/dist/{chunk-M4WA6ME7.js → chunk-RDH4XHA2.js} +3 -3
  17. package/dist/{chunk-M4WA6ME7.js.map → chunk-RDH4XHA2.js.map} +1 -1
  18. package/dist/{chunk-TAE2UB7D.js → chunk-WEHUSHVI.js} +4 -40
  19. package/dist/chunk-WEHUSHVI.js.map +1 -0
  20. package/dist/{chunk-FYBABYC7.js → chunk-Z66YUOUM.js} +3 -3
  21. package/dist/{chunk-FYBABYC7.js.map → chunk-Z66YUOUM.js.map} +1 -1
  22. package/dist/core/index.cjs +217 -0
  23. package/dist/core/index.cjs.map +1 -1
  24. package/dist/core/index.d.cts +79 -3
  25. package/dist/core/index.d.ts +79 -3
  26. package/dist/core/index.js +1 -1
  27. package/dist/ecomap/index.js +2 -2
  28. package/dist/fault-tree/index.d.cts +2 -2
  29. package/dist/fault-tree/index.d.ts +2 -2
  30. package/dist/fault-tree/index.js +2 -2
  31. package/dist/fishbone/index.js +2 -2
  32. package/dist/genogram/index.d.cts +2 -2
  33. package/dist/genogram/index.d.ts +2 -2
  34. package/dist/genogram/index.js +2 -2
  35. package/dist/geometry-P-XGqGe7.d.cts +8 -0
  36. package/dist/geometry-P-XGqGe7.d.ts +8 -0
  37. package/dist/grid-BMgUSly1.d.cts +79 -0
  38. package/dist/grid-BMgUSly1.d.ts +79 -0
  39. package/dist/index.cjs +2263 -380
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +10 -3
  42. package/dist/index.d.ts +10 -3
  43. package/dist/index.js +10 -8
  44. package/dist/labels-Br8yjc3C.d.cts +29 -0
  45. package/dist/labels-Br8yjc3C.d.ts +29 -0
  46. package/dist/labels-D1v1RWZd.d.cts +97 -0
  47. package/dist/labels-D1v1RWZd.d.ts +97 -0
  48. package/dist/layered-DmZluAqe.d.cts +72 -0
  49. package/dist/layered-DmZluAqe.d.ts +72 -0
  50. package/dist/locales/pt-br.cjs +53 -0
  51. package/dist/locales/pt-br.cjs.map +1 -1
  52. package/dist/locales/pt-br.d.cts +7 -1
  53. package/dist/locales/pt-br.d.ts +7 -1
  54. package/dist/locales/pt-br.js +50 -1
  55. package/dist/locales/pt-br.js.map +1 -1
  56. package/dist/org-chart/index.cjs +38 -36
  57. package/dist/org-chart/index.cjs.map +1 -1
  58. package/dist/org-chart/index.d.cts +5 -31
  59. package/dist/org-chart/index.d.ts +5 -31
  60. package/dist/org-chart/index.js +2 -2
  61. package/dist/pedigree/index.d.cts +2 -2
  62. package/dist/pedigree/index.d.ts +2 -2
  63. package/dist/pedigree/index.js +2 -2
  64. package/dist/phylo/index.d.cts +2 -2
  65. package/dist/phylo/index.d.ts +2 -2
  66. package/dist/phylo/index.js +2 -2
  67. package/dist/prisma/index.cjs +882 -0
  68. package/dist/prisma/index.cjs.map +1 -0
  69. package/dist/prisma/index.d.cts +174 -0
  70. package/dist/prisma/index.d.ts +174 -0
  71. package/dist/prisma/index.js +4 -0
  72. package/dist/prisma/index.js.map +1 -0
  73. package/dist/{text-DuO_PwYw.d.cts → text-DDVzpwPZ.d.cts} +1 -8
  74. package/dist/{text-DuO_PwYw.d.ts → text-DDVzpwPZ.d.ts} +1 -8
  75. package/dist/uml/index.cjs +1214 -0
  76. package/dist/uml/index.cjs.map +1 -0
  77. package/dist/uml/index.d.cts +189 -0
  78. package/dist/uml/index.d.ts +189 -0
  79. package/dist/uml/index.js +4 -0
  80. package/dist/uml/index.js.map +1 -0
  81. package/package.json +28 -2
  82. package/dist/chunk-SD4NTRBM.js +0 -171
  83. package/dist/chunk-SD4NTRBM.js.map +0 -1
  84. package/dist/chunk-TAE2UB7D.js.map +0 -1
package/README.md CHANGED
@@ -3,12 +3,14 @@
3
3
  Standards-faithful technical diagrams as **pure SVG strings**. Deterministic, zero
4
4
  runtime dependencies, server-safe (no DOM, no canvas, no clock, no randomness).
5
5
 
6
- **Seven diagrams shipped:** the **genogram** (McGoldrick family-systems notation), the
6
+ **Nine diagrams shipped:** the **genogram** (McGoldrick family-systems notation), the
7
7
  **ecomap** (radial person↔environment ties), the **fault tree** (NUREG-0492 / IEC 61025
8
8
  distinctive-shape notation), the **fishbone** (Ishikawa cause-and-effect), the
9
9
  **pedigree** (Bennett 2008 standardized clinical-genetics nomenclature), the
10
- **phylogenetic tree** (cladogram + phylogram) and the **org chart** (top-down reporting
11
- hierarchy). The architecture is built for more see the roadmap.
10
+ **phylogenetic tree** (cladogram + phylogram), the **org chart** (top-down reporting
11
+ hierarchy), the **PRISMA 2020 flow** diagram (systematic-review reporting) and the **UML
12
+ class diagram** (declared-position class model). The architecture is built for more — see
13
+ the roadmap.
12
14
 
13
15
  ## Gallery
14
16
 
@@ -33,6 +35,12 @@ examples/demo.mjs`.
33
35
  <tr>
34
36
  <td align="center" colspan="2"><strong>Org chart</strong><br><img src="examples/svg/org-chart.svg" alt="org chart example" width="470"></td>
35
37
  </tr>
38
+ <tr>
39
+ <td align="center" colspan="2"><strong>PRISMA 2020 flow</strong><br><img src="examples/svg/prisma.svg" alt="PRISMA 2020 flow diagram example" width="360"></td>
40
+ </tr>
41
+ <tr>
42
+ <td align="center" colspan="2"><strong>UML class diagram</strong><br><img src="examples/svg/uml.svg" alt="UML class diagram example" width="520"></td>
43
+ </tr>
36
44
  </table>
37
45
 
38
46
  ## Principles
@@ -253,6 +261,79 @@ structurally invalid input is **refused with coded issues** (`OrgChartValidation
253
261
  Lower-level entry points: `computeOrgChartLayout` (pure layout) and `orgChartLayoutSvg`
254
262
  (layout → string).
255
263
 
264
+ ## PRISMA 2020 flow
265
+
266
+ ```ts
267
+ import { prismaSvg } from "compasso/prisma";
268
+
269
+ const { svg } = prismaSvg({
270
+ variant: "flow",
271
+ boxes: [
272
+ { id: 1, phase: "identification", kind: "flow", column: "main", rank: 0,
273
+ heading: "Records identified", counts: [{ label: "Databases", n: 1250 }] },
274
+ { id: 2, phase: "screening", kind: "flow", column: "main", rank: 0,
275
+ heading: "Records screened", counts: [{ label: "n", n: 1124 }] },
276
+ { id: 3, phase: "screening", kind: "exclusion", column: "main", rank: 0,
277
+ heading: "Records excluded", counts: [{ label: "n", n: 796 }] },
278
+ { id: 4, phase: "included", kind: "flow", column: "main", rank: 0,
279
+ heading: "Studies included", counts: [{ label: "n", n: 55 }] },
280
+ ],
281
+ arrows: [
282
+ { id: 1, fromId: 1, toId: 2 },
283
+ { id: 2, fromId: 2, toId: 3 }, // flow → exclusion side-box
284
+ { id: 3, fromId: 2, toId: 4 },
285
+ ],
286
+ });
287
+ ```
288
+
289
+ The PRISMA 2020 flow diagram for systematic-review reporting. Boxes drop through fixed phase
290
+ bands (`identification` → `screening` → `included`); `flow` boxes stack by `rank` in the main
291
+ column, `exclusion` boxes sit to the right of their same-rank flow box. **Counts are verbatim
292
+ and never summed** — a published figure is the author's, drawn faithfully; `n: null` draws the
293
+ localized "n = —" token, never an inferred total. Variants: `flow`, `flow-with-prior` (a
294
+ two-column new/previous merge) and `flow-other-methods`. A mis-stated flow (duplicate id,
295
+ backward arrow, non-finite count, …) is **refused with coded issues** (`PrismaValidationError`),
296
+ never silently repaired. Lower-level: `computePrismaLayout` / `prismaLayoutSvg`.
297
+
298
+ ## UML class diagram
299
+
300
+ **compasso does not compute UML layout.** You declare the grid cell `(col, row)` for every
301
+ class; `packGrid` then guarantees the boxes never overlap and every edge routes through the
302
+ box-free gutters between cells. This is deliberate — provably overlap-free auto-layout of a
303
+ general graph is not possible, so positions are the author's to declare (and to nudge for
304
+ clarity), which is the honest, overlap-provable model.
305
+
306
+ ```ts
307
+ import { umlSvg } from "compasso/uml";
308
+
309
+ const { svg } = umlSvg({
310
+ classes: [
311
+ { id: 1, name: "Shape", stereotype: null, isAbstract: true, col: 0, row: 0,
312
+ attributes: [], operations: [{ visibility: "public", text: "area(): double" }] },
313
+ { id: 2, name: "Circle", stereotype: null, col: 0, row: 1,
314
+ attributes: [{ visibility: "private", text: "radius: double" }], operations: [] },
315
+ ],
316
+ relationships: [
317
+ { id: 1, kind: "generalization", sourceId: 2, targetId: 1, label: null,
318
+ sourceMultiplicity: null, targetMultiplicity: null, sourceRole: null, targetRole: null },
319
+ ],
320
+ });
321
+ ```
322
+
323
+ Three compartments (name / attributes / operations) with `+ − # ~` visibility glyphs, a
324
+ static-member underline, italic abstract names and `«stereotype»` lines. You pick a
325
+ relationship `kind` and the standard glyph follows from `RELATION_GLYPHS` — you cannot
326
+ mis-state the notation: `association`, `directed-association`, `aggregation` (hollow diamond),
327
+ `composition` (filled diamond), `generalization` / `realization` (hollow triangle; realization
328
+ dashed) and `dependency` (dashed open arrow). Multiplicities, roles and labels are drawn
329
+ verbatim, never parsed. A model error — a duplicate id, two classes in one cell, a class
330
+ inheriting itself, an inheritance cycle, or more incident edges than a box side can host — is
331
+ **refused with coded issues** (`UmlValidationError`: `duplicate-id`, `cell-collision`,
332
+ `self-generalization`, `generalization-cycle`, `too-many-side-edges`, …). For valid diagrams
333
+ the box, segment-through-box, collinear, non-orthogonal and glyph-overlap classes are all
334
+ zero; perpendicular edge crossings are not a defect (a crossing is not a lie). Lower-level:
335
+ `computeUmlLayout` / `umlLayoutSvg`.
336
+
256
337
  ## Annotations
257
338
 
258
339
  Any element you declare can be flagged `annotated: true` to draw a discreet, neutral marker —
@@ -308,8 +389,8 @@ with a negation list. PRs for new locales are welcome.
308
389
 
309
390
  The goal is a family of standards-faithful technical diagrams sharing this core
310
391
  (text metrics, stroke vocabulary, escaping, legend machinery). Shipped: genogram,
311
- ecomap, fault tree, fishbone, pedigree, phylogenetic tree, org chart. Next: flow/layered
312
- diagrams (PRISMA, UML), and symbol-library diagrams (P&ID, single-line, ladder logic) —
392
+ ecomap, fault tree, fishbone, pedigree, phylogenetic tree, org chart, PRISMA 2020 flow,
393
+ UML class diagram. Next: symbol-library diagrams (P&ID, single-line, ladder logic) —
313
394
  each built from its public standard. AST-first; text DSLs may come later.
314
395
 
315
396
  ## Contributing
@@ -1,4 +1,4 @@
1
- import { wrapLabelBalanced, clampLabel, estimateTextWidth, wrapLabel, xmlEscape, legendBlock, FONT_FAMILY, LEGEND_SWATCH_W } from './chunk-SD4NTRBM.js';
1
+ import { wrapLabelBalanced, clampLabel, estimateTextWidth, wrapLabel, xmlEscape, legendBlock, FONT_FAMILY, LEGEND_SWATCH_W } from './chunk-DVLWT565.js';
2
2
 
3
3
  // src/fishbone/render.ts
4
4
  var FISHBONE_LABELS_EN = {
@@ -248,5 +248,5 @@ function fishboneSvg(input, opts = {}) {
248
248
  }
249
249
 
250
250
  export { FISHBONE_LABELS_EN, FishboneValidationError, fishboneSvg };
251
- //# sourceMappingURL=chunk-WJYYBGZW.js.map
252
- //# sourceMappingURL=chunk-WJYYBGZW.js.map
251
+ //# sourceMappingURL=chunk-2NDET6O5.js.map
252
+ //# sourceMappingURL=chunk-2NDET6O5.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fishbone/render.ts"],"names":["w"],"mappings":"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","file":"chunk-WJYYBGZW.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"]}
1
+ {"version":3,"sources":["../src/fishbone/render.ts"],"names":["w"],"mappings":"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","file":"chunk-2NDET6O5.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"]}
@@ -1,4 +1,4 @@
1
- import { wrapLabelBalanced, clampLabel, estimateTextWidth, qualityLineStyle, EDGE_STROKE, xmlEscape, FONT_FAMILY, annotationDot, LEGEND_SWATCH_W, annotationSwatch, legendBlock } from './chunk-SD4NTRBM.js';
1
+ import { wrapLabelBalanced, clampLabel, estimateTextWidth, qualityLineStyle, EDGE_STROKE, xmlEscape, FONT_FAMILY, annotationDot, LEGEND_SWATCH_W, annotationSwatch, legendBlock } from './chunk-DVLWT565.js';
2
2
 
3
3
  // src/ecomap/render.ts
4
4
  var ECOMAP_LABELS_EN = {
@@ -201,5 +201,5 @@ function ecomapSvg(input, opts = {}) {
201
201
  }
202
202
 
203
203
  export { ECOMAP_LABELS_EN, ecomapSvg };
204
- //# sourceMappingURL=chunk-LR7BXUWM.js.map
205
- //# sourceMappingURL=chunk-LR7BXUWM.js.map
204
+ //# sourceMappingURL=chunk-3RGYLVTN.js.map
205
+ //# sourceMappingURL=chunk-3RGYLVTN.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ecomap/render.ts"],"names":["w","h"],"mappings":";;;AA+EO,IAAM,gBAAA,GAAiC;AAAA,EAC5C,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,OAAA;AAAA,IACP,OAAA,EAAS,SAAA;AAAA,IACT,QAAA,EAAU,aAAA;AAAA,IACV,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,UAAA,EAAY,cAAA;AAAA,EACZ,SAAA,EAAW,+BAAA;AAAA,EACX,SAAA,EAAW;AACb;AAuBA,IAAM,OAAA,GAAU,EAAA;AAChB,IAAM,MAAA,GAAS,EAAA;AACf,IAAM,UAAA,GAAa,EAAA;AACnB,IAAM,UAAA,GAAa,CAAA;AACnB,IAAM,YAAA,GAAe,EAAA;AACrB,IAAM,UAAA,GAAa,GAAA;AAEnB,IAAM,QAAA,GAAW,EAAA;AAEjB,IAAM,UAAA,GAAa,EAAA;AAEnB,IAAM,QAAA,GAAW,EAAA;AAEjB,IAAM,eAAA,GAAkB,CAAA;AAGxB,IAAM,WAAA,GAAc,SAAA;AACpB,IAAM,UAAA,GAAa,SAAA;AACnB,IAAM,QAAA,GAAW,SAAA;AAIjB,IAAM,QAAQ,CAAC,CAAA,KAAsB,KAAK,KAAA,CAAM,CAAA,GAAI,GAAG,CAAA,GAAI,GAAA;AAiB3D,SAAS,SAAA,CAAU,IAAA,EAAc,IAAA,EAAc,EAAA,EAAY,IAAY,OAAA,EAAyB;AAC9F,EAAA,MAAM,GAAA,GAAM,CAAA;AACZ,EAAA,MAAM,MAAA,GAAS,GAAA;AACf,EAAA,MAAM,EAAA,GAAK,OAAO,EAAA,GAAK,GAAA;AACvB,EAAA,MAAM,EAAA,GAAK,OAAO,EAAA,GAAK,GAAA;AACvB,EAAA,MAAM,KAAK,CAAC,EAAA;AACZ,EAAA,MAAM,EAAA,GAAK,EAAA;AACX,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,GAAG,KAAA,CAAM,IAAI,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAC,CAAA,CAAA;AAAA,IAC7B,CAAA,EAAG,KAAA,CAAM,EAAA,GAAK,EAAA,GAAK,MAAM,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,EAAA,GAAK,EAAA,GAAK,MAAM,CAAC,CAAA,CAAA;AAAA,IACrD,CAAA,EAAG,KAAA,CAAM,EAAA,GAAK,EAAA,GAAK,MAAM,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,EAAA,GAAK,EAAA,GAAK,MAAM,CAAC,CAAA;AAAA,GACvD,CAAE,KAAK,GAAG,CAAA;AACV,EAAA,OAAO,CAAA,iBAAA,EAAoB,MAAM,CAAA,QAAA,EAAW,QAAQ,mBAAmB,OAAO,CAAA,GAAA,CAAA;AAChF;AAQO,SAAS,SAAA,CAAU,KAAA,EAAoB,IAAA,GAAyB,EAAC,EAAW;AACjF,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,EAAA;AAClC,EAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,gBAAA;AAG9B,EAAA,MAAM,OAAkB,CAAC,GAAG,KAAA,CAAM,IAAI,EACnC,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,EAAE,EAAA,GAAK,CAAA,CAAE,EAAE,CAAA,CAC1B,GAAA,CAAI,CAAC,GAAA,KAAQ;AACZ,IAAA,MAAM,QAAQ,iBAAA,CAAkB,UAAA,CAAW,IAAI,KAAA,EAAO,IAAA,CAAK,aAAa,CAAC,CAAA;AACzE,IAAA,MAAMA,EAAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,MAAM,GAAA,CAAI,CAAC,CAAA,KAAM,iBAAA,CAAkB,CAAA,EAAG,QAAQ,CAAC,CAAC,IAAI,UAAA,GAAa,CAAA;AACvF,IAAA,MAAMC,EAAAA,GAAI,KAAA,CAAM,MAAA,GAAS,MAAA,GAAS,UAAA,GAAa,CAAA;AAC/C,IAAA,OAAO;AAAA,MACL,GAAA;AAAA,MACA,KAAA;AAAA,MACA,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,EAAA,EAAID,KAAI,CAAC,CAAA;AAAA,MACtB,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,EAAA,EAAIC,KAAI,CAAC,CAAA;AAAA,MACtB,CAAA,EAAG,CAAA;AAAA,MACH,CAAA,EAAG,CAAA;AAAA,MACH,KAAA,EAAO,CAAA;AAAA,MACP,KAAA,EAAO,gBAAA,CAAiB,GAAA,CAAI,OAAA,EAAS,KAAK,cAAc;AAAA,KAC1D;AAAA,EACF,CAAC,CAAA;AAEH,EAAA,MAAM,WAAA,GAAc,iBAAA,CAAkB,KAAA,CAAM,WAAW,CAAA;AACvD,EAAA,MAAM,UAAU,IAAA,CAAK,GAAA;AAAA,IACnB,YAAA;AAAA,IACA,IAAA,CAAK,GAAA,CAAI,GAAG,WAAA,CAAY,GAAA,CAAI,CAAC,CAAA,KAAM,iBAAA,CAAkB,CAAA,EAAG,QAAQ,CAAC,CAAC,IAAI,CAAA,GAAI,EAAA;AAAA,IACzE,WAAA,CAAY,MAAA,GAAS,MAAA,GAAU,CAAA,GAAI;AAAA,GACtC;AAMA,EAAA,MAAM,IAAI,IAAA,CAAK,MAAA;AACf,EAAA,MAAM,KAAA,GAAQ,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA,GAAI,CAAA;AAC3D,EAAA,MAAM,KAAA,GAAQ,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAE,CAAC,CAAA,GAAI,CAAA;AAC3D,EAAA,MAAM,WAAW,CAAA,GAAI,eAAA;AACrB,EAAA,MAAM,YAAY,CAAA,GAAI,CAAA,GAAK,KAAK,EAAA,GAAK,CAAA,GAAK,IAAI,IAAA,CAAK,EAAA;AACnD,EAAA,MAAM,QAAA,GAAW,KAAK,KAAA,CAAM,CAAA,GAAI,QAAQ,QAAA,EAAU,CAAA,GAAI,QAAQ,QAAQ,CAAA;AAEtE,EAAA,MAAM,cAAA,GAAiB,CAAC,KAAA,KAA0B;AAChD,IAAA,MAAM,QAAQ,KAAA,GAAQ,SAAA;AACtB,IAAA,IAAI,CAAA,IAAK,CAAA,IAAK,KAAA,IAAS,IAAA,CAAK,IAAI,OAAO,CAAA;AACvC,IAAA,OAAO,QAAA,IAAY,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAC,CAAA,CAAA;AAAA,EAC3C,CAAA;AACA,EAAA,MAAM,QAAA,GAAW,QAAQ,CAAA,GAAI,QAAA;AAC7B,EAAA,IAAI,SAAS,IAAA,CAAK,GAAA;AAAA,IAChB,UAAA;AAAA,IACA,UAAU,UAAA,GAAa,KAAA;AAAA,IACvB,cAAA,CAAe,QAAA,GAAW,CAAA,GAAI,CAAC;AAAA,GACjC;AACA,EAAA,IAAI,QAAA,EAAU;AAIZ,IAAA,MAAM,YAAY,CAAC,CAAA,KACjB,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,GAAA,CAAK,CAAA,GAAI,QAAA,KAAa,CAAA,GAAI,IAAI,CAAA,IAAK,CAAA,GAAI,YAAY,IAAA,CAAK,GAAA,CAAI,SAAS,CAAC,CAAA;AACtF,IAAA,OAAO,SAAA,CAAU,MAAM,CAAA,GAAI,QAAA,EAAU,MAAA,IAAU,CAAA;AAAA,EACjD;AACA,EAAA,MAAM,SAAS,MAAA,GAAS,QAAA;AAExB,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,IAAA,MAAM,CAAA,GAAI,KAAK,CAAC,CAAA;AAChB,IAAA,CAAA,CAAE,KAAA,GAAQ,CAAC,IAAA,CAAK,EAAA,GAAK,IAAK,CAAA,GAAI,IAAA,CAAK,KAAK,CAAA,GAAK,CAAA;AAC7C,IAAA,MAAM,CAAA,GAAI,QAAA,IAAY,CAAA,GAAI,CAAA,KAAM,IAAI,MAAA,GAAS,MAAA;AAC7C,IAAA,CAAA,CAAE,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,EAAE,KAAK,CAAA;AAC1B,IAAA,CAAA,CAAE,CAAA,GAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,EAAE,KAAK,CAAA;AAAA,EAC5B;AAGA,EAAA,IAAI,OAAO,CAAC,OAAA;AACZ,EAAA,IAAI,OAAO,CAAC,OAAA;AACZ,EAAA,IAAI,IAAA,GAAO,OAAA;AACX,EAAA,IAAI,IAAA,GAAO,OAAA;AACX,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,IAAA,GAAO,KAAK,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA,GAAI,EAAE,EAAE,CAAA;AAChC,IAAA,IAAA,GAAO,KAAK,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA,GAAI,EAAE,EAAE,CAAA;AAChC,IAAA,IAAA,GAAO,KAAK,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA,GAAI,EAAE,EAAE,CAAA;AAChC,IAAA,IAAA,GAAO,KAAK,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA,GAAI,EAAE,EAAE,CAAA;AAAA,EAClC;AACA,EAAA,MAAM,KAAK,OAAA,GAAU,IAAA;AACrB,EAAA,MAAM,KAAK,OAAA,GAAU,IAAA;AACrB,EAAA,MAAM,EAAA,GAAK,EAAA;AACX,EAAA,MAAM,EAAA,GAAK,EAAA;AACX,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,CAAA,CAAE,CAAA,IAAK,EAAA;AACP,IAAA,CAAA,CAAE,CAAA,IAAK,EAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAA,GAAQ,IAAA,GAAO,IAAA,GAAO,OAAA,GAAU,CAAA;AACpC,EAAA,IAAI,MAAA,GAAS,IAAA,GAAO,IAAA,GAAO,OAAA,GAAU,CAAA;AAErC,EAAA,MAAM,QAAkB,EAAC;AAGzB,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,MAAM,EAAA,GAAA,CAAM,EAAA,GAAK,CAAA,CAAE,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,CAAA,EAAG,EAAA,GAAK,CAAA,CAAE,CAAC,CAAA;AACrD,IAAA,MAAM,EAAA,GAAA,CAAM,EAAA,GAAK,CAAA,CAAE,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,CAAA,EAAG,EAAA,GAAK,CAAA,CAAE,CAAC,CAAA;AAErD,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,CAAE,EAAA,EAAI,EAAA,GAAK,CAAA,CAAE,EAAE,CAAA;AACjD,IAAA,MAAM,EAAA,GAAK,CAAA,CAAE,CAAA,GAAI,EAAA,GAAK,KAAA;AACtB,IAAA,MAAM,EAAA,GAAK,CAAA,CAAE,CAAA,GAAI,EAAA,GAAK,KAAA;AAEtB,IAAA,MAAM,EAAA,GAAK,KAAK,EAAA,GAAK,OAAA;AACrB,IAAA,MAAM,EAAA,GAAK,KAAK,EAAA,GAAK,OAAA;AAErB,IAAA,MAAM,GAAA,GAAM,WAAA,CAAY,CAAA,CAAE,KAAK,CAAA;AAC/B,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,KAAS,IAAA,GAAO,KAAK,CAAA,mBAAA,EAAsB,GAAA,CAAI,IAAA,CAAK,CAAC,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,IAAA,CAAK,CAAC,CAAC,CAAA,CAAA,CAAA;AAC1F,IAAA,MAAM,QAAQ,CAAA,CAAE,GAAA,CAAI,UAAU,CAAA,CAAE,GAAA,CAAI,YAAY,IAAA,GAAO,CAAA,EAAG,CAAA,CAAE,GAAA,CAAI,KAAK,CAAA,MAAA,EAAM,CAAA,CAAE,IAAI,OAAO,CAAA,CAAA,GAAK,EAAE,GAAA,CAAI,KAAA,CAAA;AACnG,IAAA,MAAM,IAAA,GAAiB;AAAA,MACrB,CAAA,UAAA,EAAa,KAAA,CAAM,EAAE,CAAC,CAAA,MAAA,EAAS,MAAM,EAAE,CAAC,CAAA,MAAA,EAAS,KAAA,CAAM,EAAE,CAAC,SAAS,KAAA,CAAM,EAAE,CAAC,CAAA,UAAA,EAAa,QAAQ,CAAA,gBAAA,EAAmB,GAAA,CAAI,KAAK,CAAA,kBAAA,EAAqB,GAAA,CAAI,OAAO,CAAA,CAAA,EAAI,QAAQ,CAAA,EAAA;AAAA,KAC3K;AAIA,IAAA,IAAI,EAAE,GAAA,CAAI,SAAA,KAAc,QAAQ,CAAA,CAAE,GAAA,CAAI,cAAc,MAAA,EAAQ;AAC1D,MAAA,IAAA,CAAK,IAAA,CAAK,UAAU,EAAA,EAAI,EAAA,EAAI,IAAI,EAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IAClD;AACA,IAAA,IAAI,EAAE,GAAA,CAAI,SAAA,KAAc,SAAS,CAAA,CAAE,GAAA,CAAI,cAAc,MAAA,EAAQ;AAC3D,MAAA,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,EAAA,EAAI,CAAC,IAAI,CAAC,EAAA,EAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IACpD;AACA,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,iBAAA,EAAoB,CAAA,CAAE,GAAA,CAAI,EAAE,CAAA,SAAA,EAAY,SAAA,CAAU,KAAK,CAAC,CAAA,QAAA,EAAW,IAAA,CAAK,IAAA,CAAK,EAAE,CAAC,CAAA,IAAA,CAAM,CAAA;AAAA,EACnG;AAGA,EAAA;AACE,IAAA,MAAM,SAAS,WAAA,CACZ,GAAA;AAAA,MACC,CAAC,MAAM,CAAA,KACL,CAAA,UAAA,EAAa,MAAM,EAAE,CAAC,CAAA,KAAA,EAAQ,KAAA,CAAM,EAAA,GAAA,CAAO,WAAA,CAAY,SAAS,CAAA,IAAK,MAAA,GAAU,CAAA,GAAI,CAAA,GAAI,MAAA,GAAS,QAAA,GAAW,IAAI,CAAC,CAAA,EAAA,EAAK,SAAA,CAAU,IAAI,CAAC,CAAA,QAAA;AAAA,KACxI,CACC,KAAK,EAAE,CAAA;AACV,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,sCAAA,EAAyC,SAAA,CAAU,KAAA,CAAM,WAAW,CAAC,CAAA,oBAAA,EACpD,KAAA,CAAM,EAAE,CAAC,CAAA,MAAA,EAAS,KAAA,CAAM,EAAE,CAAC,QAAQ,KAAA,CAAM,OAAO,CAAC,CAAA,6BAAA,EAAgC,WAAW,CAAA,4DAAA,EAChE,WAAW,CAAA,aAAA,EAAgB,QAAQ,CAAA,QAAA,EAAW,UAAU,CAAA,EAAA,EAAK,MAAM,CAAA,WAAA;AAAA,KAClH;AAAA,EACF;AAGA,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,MAAM,MAAA,GAAS,EAAE,KAAA,CACd,GAAA;AAAA,MACC,CAAC,IAAA,EAAM,CAAA,KACL,CAAA,UAAA,EAAa,KAAA,CAAM,CAAA,CAAE,CAAC,CAAC,CAAA,KAAA,EAAQ,KAAA,CAAM,CAAA,CAAE,CAAA,GAAA,CAAM,CAAA,CAAE,MAAM,MAAA,GAAS,CAAA,IAAK,MAAA,GAAU,CAAA,GAAI,CAAA,GAAI,MAAA,GAAS,QAAA,GAAW,IAAI,CAAC,CAAA,EAAA,EAAK,SAAA,CAAU,IAAI,CAAC,CAAA,QAAA;AAAA,KACtI,CACC,KAAK,EAAE,CAAA;AACV,IAAA,MAAM,MAAA,GAAmB;AAAA,MACvB,CAAA,OAAA,EAAU,SAAA,CAAU,CAAA,CAAE,GAAA,CAAI,KAAK,CAAC,CAAA,QAAA,CAAA;AAAA,MAChC,CAAA,aAAA,EAAgB,MAAM,CAAA,CAAE,CAAC,CAAC,CAAA,MAAA,EAAS,KAAA,CAAM,EAAE,CAAC,CAAC,SAAS,KAAA,CAAM,CAAA,CAAE,EAAE,CAAC,CAAA,MAAA,EAAS,MAAM,CAAA,CAAE,EAAE,CAAC,CAAA,6BAAA,EAAgC,WAAW,CAAA,sBAAA,CAAA;AAAA,MAChI,2CAA2C,WAAW,CAAA,aAAA,EAAgB,QAAQ,CAAA,QAAA,EAAW,UAAU,KAAK,MAAM,CAAA,OAAA;AAAA,KAChH;AACA,IAAA,IAAI,CAAA,CAAE,IAAI,SAAA,EAAW;AACnB,MAAA,MAAA,CAAO,IAAA,CAAK,aAAA,CAAc,CAAA,CAAE,CAAA,GAAI,GAAA,GAAM,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,CAAA,GAAI,GAAA,GAAM,CAAA,CAAE,EAAE,CAAC,CAAA;AAAA,IAC/D;AACA,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,wBAAA,EAA2B,CAAA,CAAE,GAAA,CAAI,EAAE,KAAK,MAAA,CAAO,IAAA,CAAK,EAAE,CAAC,CAAA,IAAA,CAAM,CAAA;AAAA,EAC1E;AAGA,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,KAAA,IAAS,IAAA,CAAK,SAAS,CAAA,EAAG;AAC5C,IAAA,MAAM,UAAyB,EAAC;AAEhC,IAAA,IAAI,KAAK,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,KAAA,KAAU,OAAO,CAAA,EAAG;AACzC,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAA,KACV,aAAa,CAAC,CAAA,MAAA,EAAS,CAAC,CAAA,MAAA,EAAS,CAAA,GAAI,eAAe,SAAS,CAAC,CAAA,UAAA,EAAa,QAAQ,CAAA,gBAAA,EAAmB,WAAA,CAAY,MAAM,KAAK,CAAA,kBAAA,EAAqB,WAAA,CAAY,KAAA,CAAM,OAAO,CAAA,GAAA,CAAA;AAAA,QAC7K,OAAO,MAAA,CAAO;AAAA,OACf,CAAA;AAAA,IACH;AACA,IAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,IAAA,CAAK,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,KAAK,CAAC,CAAA;AACnD,IAAA,KAAA,MAAW,SAAS,CAAC,OAAA,EAAS,SAAA,EAAW,UAAA,EAAY,QAAQ,CAAA,EAAY;AACvE,MAAA,IAAI,CAAC,UAAA,CAAW,GAAA,CAAI,KAAK,CAAA,EAAG;AAC5B,MAAA,MAAM,GAAA,GAAM,YAAY,KAAK,CAAA;AAC7B,MAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,KAAS,IAAA,GAAO,KAAK,CAAA,mBAAA,EAAsB,GAAA,CAAI,IAAA,CAAK,CAAC,CAAC,CAAA,CAAA,EAAI,GAAA,CAAI,IAAA,CAAK,CAAC,CAAC,CAAA,CAAA,CAAA;AAC1F,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAA,KACV,aAAa,CAAC,CAAA,MAAA,EAAS,CAAC,CAAA,MAAA,EAAS,CAAA,GAAI,eAAe,SAAS,CAAC,CAAA,UAAA,EAAa,QAAQ,CAAA,gBAAA,EAAmB,GAAA,CAAI,KAAK,CAAA,kBAAA,EAAqB,GAAA,CAAI,OAAO,CAAA,CAAA,EAAI,QAAQ,CAAA,EAAA,CAAA;AAAA,QAC7J,KAAA,EAAO,MAAA,CAAO,UAAA,CAAW,KAAK;AAAA,OAC/B,CAAA;AAAA,IACH;AACA,IAAA,IAAI,IAAA,CAAK,KAAK,CAAC,CAAA,KAAM,EAAE,GAAA,CAAI,SAAA,KAAc,IAAI,CAAA,EAAG;AAC9C,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAA,KACV,aAAa,CAAC,CAAA,MAAA,EAAS,CAAC,CAAA,MAAA,EAAS,CAAA,GAAI,eAAA,GAAkB,CAAC,CAAA,MAAA,EAAS,CAAC,CAAA,UAAA,EAAa,QAAQ,CAAA,4CAAA,CAAA,GACvF,SAAA,CAAU,IAAI,eAAA,EAAiB,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,CAAA;AAAA,QAC9C,OAAO,MAAA,CAAO;AAAA,OACf,CAAA;AAAA,IACH;AACA,IAAA,IAAI,IAAA,CAAK,eAAA,KAAoB,MAAA,IAAa,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,GAAA,CAAI,SAAS,CAAA,EAAG;AAC3E,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,QAAQ,CAAC,CAAA,EAAG,CAAA,KAAM,gBAAA,CAAiB,GAAG,CAAC,CAAA;AAAA,QACvC,OAAO,IAAA,CAAK;AAAA,OACb,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,OAAA,EAAS,MAAM,CAAA;AACzC,MAAA,KAAA,CAAM,IAAA,CAAK,MAAM,GAAG,CAAA;AACpB,MAAA,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA;AACnC,MAAA,MAAA,GAAS,KAAA,CAAM,MAAA;AAAA,IACjB;AAAA,EACF;AAEA,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA;AACzB,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAC1B,EAAA,OACE,wDAAwD,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA,SAAA,EAAY,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,yBAAA,EAA4B,SAAA,CAAU,OAAO,SAAS,CAAC,OAChJ,KAAA,CAAM,IAAA,CAAK,EAAE,CAAA,GACb,CAAA,MAAA,CAAA;AAEJ","file":"chunk-LR7BXUWM.js","sourcesContent":["// Radial ecomap renderer — center node + declared external ties on 1–2 concentric\n// rings, as a SELF-CONTAINED SVG string. Pure (data in, string out), deterministic\n// (same input → same SVG), no DOM, no dependencies.\n//\n// HONESTY RULE: the diagram presents ONLY what the caller declared. A tie's line style\n// is a conservative lexical hint from the author's own quality word (ambiguous/null →\n// neutral solid line, never a synthesized value); an arrowhead appears ONLY when a\n// direction was declared (null = no arrow, never a default). The verbatim text always\n// rides the element's <title>, so a style never replaces what was said.\n//\n// Ring assignment is a deterministic PRESENTATION rule — by input order (sorted by id),\n// alternating rings when the set is large — never by any reading of the tie's content.\n//\n// Every interpolated text passes xmlEscape (labels are author-controlled; the SVG may\n// be inlined via innerHTML or embedded in a PDF). All presentation attributes are\n// literal: a standalone SVG has no stylesheet. Arrowheads are explicit polygons, NOT\n// <marker> defs — marker support is unreliable in SVG-to-PDF embedders, and inline\n// polygons also avoid def-id collisions when several SVGs share one document.\n\nimport {\n EDGE_STROKE,\n FONT_FAMILY,\n LEGEND_SWATCH_W,\n annotationDot,\n annotationSwatch,\n clampLabel,\n estimateTextWidth,\n legendBlock,\n qualityLineStyle,\n wrapLabelBalanced,\n xmlEscape,\n type EdgeLineStyle,\n type LegendEntry,\n type QualityLexicon,\n} from \"../core\";\n\n// ── Input model ───────────────────────────────────────────────────────────────\n\n/** Declared flow of the tie, relative to the CENTER: \"in\" = toward the center. */\nexport type EcomapDirection = \"in\" | \"out\" | \"both\";\n\n/**\n * One declared tie between the center (person/household) and an external system\n * (work, faith, friends, healthcare…). `label` is the author's own naming, kept\n * verbatim. `quality` is the author's verbatim wording about the tie (null = not\n * declared = neutral line). `direction` follows the same doctrine: null = no arrow.\n */\nexport interface EcomapTie {\n id: number;\n label: string;\n quality: string | null;\n direction: EcomapDirection | null;\n /** Optional verbatim <title> override (defaults to \"label · quality\"). */\n title?: string;\n /**\n * Caller-asserted flag: this tie was annotated (e.g. edited by a professional).\n * compasso never interprets this — it only draws a neutral marker and, when\n * `annotationLabel` is supplied, adds a legend row. Default undefined = not annotated.\n */\n annotated?: boolean;\n}\n\nexport interface EcomapInput {\n /** Center node label (e.g. the person or household the map is about). */\n centerLabel: string;\n ties: EcomapTie[];\n}\n\n// ── Display vocabulary ────────────────────────────────────────────────────────\n\nexport interface EcomapLabels {\n bondStyles: Record<Exclude<EdgeLineStyle, \"plain\">, string>;\n /** Legend label for the neutral solid line (a declared tie with no styled quality). */\n neutralTie: string;\n /** Legend label for the arrowhead (declared direction of the tie). */\n direction: string;\n ariaLabel: string;\n}\n\nexport const ECOMAP_LABELS_EN: EcomapLabels = {\n bondStyles: {\n close: \"Close\",\n distant: \"Distant\",\n conflict: \"Conflictual\",\n cutoff: \"Cut off (no contact)\",\n },\n neutralTie: \"Declared tie\",\n direction: \"Declared direction of the tie\",\n ariaLabel: \"Ecomap\",\n};\n\nexport interface EcomapSvgOptions {\n /** Display clamp per tie label (verbatim text stays in the <title>). */\n maxLabelChars?: number;\n /** Node label font size (px); default 12. */\n fontSize?: number;\n /** Set false to suppress the legend (compact preview); default true. */\n legend?: boolean;\n /** Quality-word lexicon — English default; see `compasso/locales/pt-br`. */\n qualityLexicon?: QualityLexicon;\n /** Display vocabulary — English default; see locale packs. */\n labels?: EcomapLabels;\n /**\n * When provided AND at least one tie has `annotated: true`, a legend row is added\n * with this label (used-keys-only pattern). compasso supplies no default — the caller\n * owns the wording (domain-agnostic).\n */\n annotationLabel?: string;\n}\n\n// ── Geometry constants ────────────────────────────────────────────────────────\n\nconst PADDING = 32;\nconst LINE_H = 14;\nconst NODE_PAD_X = 14;\nconst NODE_PAD_Y = 9;\nconst CENTER_MIN_R = 36;\nconst RING_MIN_R = 150;\n/** Min gap between two adjacent node boxes on the same ring. */\nconst NODE_GAP = 24;\n/** Radial gap between the center circle and the inner ring's nearest box edge. */\nconst RADIAL_GAP = 40;\n/** Radial gap between the two rings when the set is large enough to split. */\nconst RING_GAP = 26;\n/** Above this many ties the layout alternates between two rings. */\nconst SINGLE_RING_MAX = 8;\n\n// Ink (zinc ramp on white — matches the genogram emitter).\nconst NODE_STROKE = \"#52525b\";\nconst LABEL_FILL = \"#3f3f46\";\nconst EDGE_INK = \"#71717a\";\n// Legend row geometry + the swatch span both live in ../core (LEGEND_SWATCH_W imported\n// above for the swatch closures); the row/text/width/height bookkeeping is legendBlock.\n\nconst round = (n: number): number => Math.round(n * 100) / 100;\n\n// ── Internal shapes ───────────────────────────────────────────────────────────\n\ninterface SatNode {\n tie: EcomapTie;\n lines: string[];\n rx: number;\n ry: number;\n /** Filled once ring radii are known. */\n x: number;\n y: number;\n angle: number;\n style: EdgeLineStyle;\n}\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\n/**\n * Renders a declared ecomap to a self-contained SVG string. Deterministic: same data →\n * same SVG. Zero ties yield a valid center-only SVG; callers decide their own empty\n * state before calling. The root keeps numeric width/height attributes plus a matching\n * viewBox (PDF-embedder contract).\n */\nexport function ecomapSvg(input: EcomapInput, opts: EcomapSvgOptions = {}): string {\n const fontSize = opts.fontSize ?? 12;\n const labels = opts.labels ?? ECOMAP_LABELS_EN;\n\n // ── Measure every satellite box (deterministic order: by tie id). ───────────\n const sats: SatNode[] = [...input.ties]\n .sort((a, b) => a.id - b.id)\n .map((tie) => {\n const lines = wrapLabelBalanced(clampLabel(tie.label, opts.maxLabelChars));\n const w = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize))) + NODE_PAD_X * 2;\n const h = lines.length * LINE_H + NODE_PAD_Y * 2;\n return {\n tie,\n lines,\n rx: Math.max(40, w / 2),\n ry: Math.max(20, h / 2),\n x: 0,\n y: 0,\n angle: 0,\n style: qualityLineStyle(tie.quality, opts.qualityLexicon),\n };\n });\n\n const centerLines = wrapLabelBalanced(input.centerLabel);\n const centerR = Math.max(\n CENTER_MIN_R,\n Math.max(...centerLines.map((l) => estimateTextWidth(l, fontSize))) / 2 + 12,\n (centerLines.length * LINE_H) / 2 + 12,\n );\n\n // ── Ring radii: wide enough that NO two node boxes can ever touch. The safe\n // center-to-center distance is the diagonal of the worst-case box pair: if the\n // euclidean distance is ≥ hypot(width-sum, height-sum), the axis-aligned boxes\n // cannot overlap in both axes at once. Overlap-proof by construction, not by luck.\n const n = sats.length;\n const maxRx = n > 0 ? Math.max(...sats.map((s) => s.rx)) : 0;\n const maxRy = n > 0 ? Math.max(...sats.map((s) => s.ry)) : 0;\n const twoRings = n > SINGLE_RING_MAX;\n const stepAngle = n > 0 ? (Math.PI * 2) / n : Math.PI;\n const safeDist = Math.hypot(2 * maxRx + NODE_GAP, 2 * maxRy + NODE_GAP);\n /** Min radius so two nodes `steps` angular steps apart on the SAME ring clear safeDist. */\n const sameRingRadius = (steps: number): number => {\n const theta = steps * stepAngle;\n if (n <= 1 || theta >= Math.PI) return 0; // ≤2 nodes on the ring: no chord constraint\n return safeDist / (2 * Math.sin(theta / 2));\n };\n const ringStep = maxRy * 2 + RING_GAP;\n let innerR = Math.max(\n RING_MIN_R,\n centerR + RADIAL_GAP + maxRy,\n sameRingRadius(twoRings ? 2 : 1),\n );\n if (twoRings) {\n // Cross-ring neighbors sit 1 step apart at radii r and r+ringStep. Their distance\n // sqrt(r² + (r+s)² − 2r(r+s)cosΔ) grows monotonically with r — widen the inner ring\n // in fixed increments until it clears safeDist (deterministic, always terminates).\n const crossDist = (r: number): number =>\n Math.sqrt(r * r + (r + ringStep) ** 2 - 2 * r * (r + ringStep) * Math.cos(stepAngle));\n while (crossDist(innerR) < safeDist) innerR += 8;\n }\n const outerR = innerR + ringStep;\n\n for (let i = 0; i < n; i++) {\n const s = sats[i]!;\n s.angle = -Math.PI / 2 + (i * Math.PI * 2) / n;\n const r = twoRings && i % 2 === 1 ? outerR : innerR;\n s.x = r * Math.cos(s.angle);\n s.y = r * Math.sin(s.angle);\n }\n\n // ── Canvas bounds (center at origin until here), then shift positive. ───────\n let minX = -centerR;\n let minY = -centerR;\n let maxX = centerR;\n let maxY = centerR;\n for (const s of sats) {\n minX = Math.min(minX, s.x - s.rx);\n minY = Math.min(minY, s.y - s.ry);\n maxX = Math.max(maxX, s.x + s.rx);\n maxY = Math.max(maxY, s.y + s.ry);\n }\n const dx = PADDING - minX;\n const dy = PADDING - minY;\n const cx = dx;\n const cy = dy;\n for (const s of sats) {\n s.x += dx;\n s.y += dy;\n }\n let width = maxX - minX + PADDING * 2;\n let height = maxY - minY + PADDING * 2;\n\n const parts: string[] = [];\n\n // ── Tie lines first (nodes sit on top), one group per declared tie. ─────────\n for (const s of sats) {\n const ux = (cx - s.x) / Math.hypot(cx - s.x, cy - s.y);\n const uy = (cy - s.y) / Math.hypot(cx - s.x, cy - s.y);\n // Node-edge endpoint: ellipse boundary along the unit vector toward the center.\n const scale = 1 / Math.hypot(ux / s.rx, uy / s.ry);\n const x1 = s.x + ux * scale;\n const y1 = s.y + uy * scale;\n // Center-edge endpoint: circle boundary along the same direction.\n const x2 = cx - ux * centerR;\n const y2 = cy - uy * centerR;\n\n const ink = EDGE_STROKE[s.style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} · ${s.tie.quality}` : s.tie.label);\n const body: string[] = [\n `<line x1=\"${round(x1)}\" y1=\"${round(y1)}\" x2=\"${round(x2)}\" y2=\"${round(y2)}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n ];\n // Arrowheads ONLY for a declared direction. \"in\" points at the center, \"out\" at\n // the external system, \"both\" draws both. Tips back off the boundary so the\n // triangle never pokes into the shape.\n if (s.tie.direction === \"in\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x2, y2, ux, uy, ink.opacity)); // tip at the center edge\n }\n if (s.tie.direction === \"out\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x1, y1, -ux, -uy, ink.opacity)); // tip at the system edge\n }\n parts.push(`<g data-edge-id=\"${s.tie.id}\"><title>${xmlEscape(title)}</title>${body.join(\"\")}</g>`);\n }\n\n // ── Center node. ─────────────────────────────────────────────────────────────\n {\n const tspans = centerLines\n .map(\n (line, i) =>\n `<tspan x=\"${round(cx)}\" y=\"${round(cy - ((centerLines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n parts.push(\n `<g data-individual-id=\"center\"><title>${xmlEscape(input.centerLabel)}</title>` +\n `<circle cx=\"${round(cx)}\" cy=\"${round(cy)}\" r=\"${round(centerR)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"2\"/>` +\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text></g>`,\n );\n }\n\n // ── Satellite nodes. ─────────────────────────────────────────────────────────\n for (const s of sats) {\n const tspans = s.lines\n .map(\n (line, i) =>\n `<tspan x=\"${round(s.x)}\" y=\"${round(s.y - ((s.lines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n const pieces: string[] = [\n `<title>${xmlEscape(s.tie.label)}</title>`,\n `<ellipse cx=\"${round(s.x)}\" cy=\"${round(s.y)}\" rx=\"${round(s.rx)}\" ry=\"${round(s.ry)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"1.5\"/>`,\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text>`,\n ];\n if (s.tie.annotated) {\n pieces.push(annotationDot(s.x + 0.7 * s.rx, s.y - 0.7 * s.ry));\n }\n parts.push(`<g data-individual-id=\"e${s.tie.id}\">${pieces.join(\"\")}</g>`);\n }\n\n // ── Minimal legend: only entries actually used. ──────────────────────────────\n if (opts.legend !== false && sats.length > 0) {\n const entries: LegendEntry[] = [];\n\n if (sats.some((s) => s.style === \"plain\")) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${EDGE_STROKE.plain.width}\" stroke-opacity=\"${EDGE_STROKE.plain.opacity}\"/>`,\n label: labels.neutralTie,\n });\n }\n const stylesUsed = new Set(sats.map((s) => s.style));\n for (const style of [\"close\", \"distant\", \"conflict\", \"cutoff\"] as const) {\n if (!stylesUsed.has(style)) continue;\n const ink = EDGE_STROKE[style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n label: labels.bondStyles[style],\n });\n }\n if (sats.some((s) => s.tie.direction !== null)) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W - 8}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"1.5\" stroke-opacity=\"0.75\"/>` +\n arrowHead(x + LEGEND_SWATCH_W, y, 1, 0, 0.75),\n label: labels.direction,\n });\n }\n if (opts.annotationLabel !== undefined && sats.some((s) => s.tie.annotated)) {\n entries.push({\n swatch: (x, y) => annotationSwatch(x, y),\n label: opts.annotationLabel,\n });\n }\n\n if (entries.length > 0) {\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\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"]}
1
+ {"version":3,"sources":["../src/ecomap/render.ts"],"names":["w","h"],"mappings":"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","file":"chunk-3RGYLVTN.js","sourcesContent":["// Radial ecomap renderer — center node + declared external ties on 1–2 concentric\n// rings, as a SELF-CONTAINED SVG string. Pure (data in, string out), deterministic\n// (same input → same SVG), no DOM, no dependencies.\n//\n// HONESTY RULE: the diagram presents ONLY what the caller declared. A tie's line style\n// is a conservative lexical hint from the author's own quality word (ambiguous/null →\n// neutral solid line, never a synthesized value); an arrowhead appears ONLY when a\n// direction was declared (null = no arrow, never a default). The verbatim text always\n// rides the element's <title>, so a style never replaces what was said.\n//\n// Ring assignment is a deterministic PRESENTATION rule — by input order (sorted by id),\n// alternating rings when the set is large — never by any reading of the tie's content.\n//\n// Every interpolated text passes xmlEscape (labels are author-controlled; the SVG may\n// be inlined via innerHTML or embedded in a PDF). All presentation attributes are\n// literal: a standalone SVG has no stylesheet. Arrowheads are explicit polygons, NOT\n// <marker> defs — marker support is unreliable in SVG-to-PDF embedders, and inline\n// polygons also avoid def-id collisions when several SVGs share one document.\n\nimport {\n EDGE_STROKE,\n FONT_FAMILY,\n LEGEND_SWATCH_W,\n annotationDot,\n annotationSwatch,\n clampLabel,\n estimateTextWidth,\n legendBlock,\n qualityLineStyle,\n wrapLabelBalanced,\n xmlEscape,\n type EdgeLineStyle,\n type LegendEntry,\n type QualityLexicon,\n} from \"../core\";\n\n// ── Input model ───────────────────────────────────────────────────────────────\n\n/** Declared flow of the tie, relative to the CENTER: \"in\" = toward the center. */\nexport type EcomapDirection = \"in\" | \"out\" | \"both\";\n\n/**\n * One declared tie between the center (person/household) and an external system\n * (work, faith, friends, healthcare…). `label` is the author's own naming, kept\n * verbatim. `quality` is the author's verbatim wording about the tie (null = not\n * declared = neutral line). `direction` follows the same doctrine: null = no arrow.\n */\nexport interface EcomapTie {\n id: number;\n label: string;\n quality: string | null;\n direction: EcomapDirection | null;\n /** Optional verbatim <title> override (defaults to \"label · quality\"). */\n title?: string;\n /**\n * Caller-asserted flag: this tie was annotated (e.g. edited by a professional).\n * compasso never interprets this — it only draws a neutral marker and, when\n * `annotationLabel` is supplied, adds a legend row. Default undefined = not annotated.\n */\n annotated?: boolean;\n}\n\nexport interface EcomapInput {\n /** Center node label (e.g. the person or household the map is about). */\n centerLabel: string;\n ties: EcomapTie[];\n}\n\n// ── Display vocabulary ────────────────────────────────────────────────────────\n\nexport interface EcomapLabels {\n bondStyles: Record<Exclude<EdgeLineStyle, \"plain\">, string>;\n /** Legend label for the neutral solid line (a declared tie with no styled quality). */\n neutralTie: string;\n /** Legend label for the arrowhead (declared direction of the tie). */\n direction: string;\n ariaLabel: string;\n}\n\nexport const ECOMAP_LABELS_EN: EcomapLabels = {\n bondStyles: {\n close: \"Close\",\n distant: \"Distant\",\n conflict: \"Conflictual\",\n cutoff: \"Cut off (no contact)\",\n },\n neutralTie: \"Declared tie\",\n direction: \"Declared direction of the tie\",\n ariaLabel: \"Ecomap\",\n};\n\nexport interface EcomapSvgOptions {\n /** Display clamp per tie label (verbatim text stays in the <title>). */\n maxLabelChars?: number;\n /** Node label font size (px); default 12. */\n fontSize?: number;\n /** Set false to suppress the legend (compact preview); default true. */\n legend?: boolean;\n /** Quality-word lexicon — English default; see `compasso/locales/pt-br`. */\n qualityLexicon?: QualityLexicon;\n /** Display vocabulary — English default; see locale packs. */\n labels?: EcomapLabels;\n /**\n * When provided AND at least one tie has `annotated: true`, a legend row is added\n * with this label (used-keys-only pattern). compasso supplies no default — the caller\n * owns the wording (domain-agnostic).\n */\n annotationLabel?: string;\n}\n\n// ── Geometry constants ────────────────────────────────────────────────────────\n\nconst PADDING = 32;\nconst LINE_H = 14;\nconst NODE_PAD_X = 14;\nconst NODE_PAD_Y = 9;\nconst CENTER_MIN_R = 36;\nconst RING_MIN_R = 150;\n/** Min gap between two adjacent node boxes on the same ring. */\nconst NODE_GAP = 24;\n/** Radial gap between the center circle and the inner ring's nearest box edge. */\nconst RADIAL_GAP = 40;\n/** Radial gap between the two rings when the set is large enough to split. */\nconst RING_GAP = 26;\n/** Above this many ties the layout alternates between two rings. */\nconst SINGLE_RING_MAX = 8;\n\n// Ink (zinc ramp on white — matches the genogram emitter).\nconst NODE_STROKE = \"#52525b\";\nconst LABEL_FILL = \"#3f3f46\";\nconst EDGE_INK = \"#71717a\";\n// Legend row geometry + the swatch span both live in ../core (LEGEND_SWATCH_W imported\n// above for the swatch closures); the row/text/width/height bookkeeping is legendBlock.\n\nconst round = (n: number): number => Math.round(n * 100) / 100;\n\n// ── Internal shapes ───────────────────────────────────────────────────────────\n\ninterface SatNode {\n tie: EcomapTie;\n lines: string[];\n rx: number;\n ry: number;\n /** Filled once ring radii are known. */\n x: number;\n y: number;\n angle: number;\n style: EdgeLineStyle;\n}\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\n/**\n * Renders a declared ecomap to a self-contained SVG string. Deterministic: same data →\n * same SVG. Zero ties yield a valid center-only SVG; callers decide their own empty\n * state before calling. The root keeps numeric width/height attributes plus a matching\n * viewBox (PDF-embedder contract).\n */\nexport function ecomapSvg(input: EcomapInput, opts: EcomapSvgOptions = {}): string {\n const fontSize = opts.fontSize ?? 12;\n const labels = opts.labels ?? ECOMAP_LABELS_EN;\n\n // ── Measure every satellite box (deterministic order: by tie id). ───────────\n const sats: SatNode[] = [...input.ties]\n .sort((a, b) => a.id - b.id)\n .map((tie) => {\n const lines = wrapLabelBalanced(clampLabel(tie.label, opts.maxLabelChars));\n const w = Math.max(...lines.map((l) => estimateTextWidth(l, fontSize))) + NODE_PAD_X * 2;\n const h = lines.length * LINE_H + NODE_PAD_Y * 2;\n return {\n tie,\n lines,\n rx: Math.max(40, w / 2),\n ry: Math.max(20, h / 2),\n x: 0,\n y: 0,\n angle: 0,\n style: qualityLineStyle(tie.quality, opts.qualityLexicon),\n };\n });\n\n const centerLines = wrapLabelBalanced(input.centerLabel);\n const centerR = Math.max(\n CENTER_MIN_R,\n Math.max(...centerLines.map((l) => estimateTextWidth(l, fontSize))) / 2 + 12,\n (centerLines.length * LINE_H) / 2 + 12,\n );\n\n // ── Ring radii: wide enough that NO two node boxes can ever touch. The safe\n // center-to-center distance is the diagonal of the worst-case box pair: if the\n // euclidean distance is ≥ hypot(width-sum, height-sum), the axis-aligned boxes\n // cannot overlap in both axes at once. Overlap-proof by construction, not by luck.\n const n = sats.length;\n const maxRx = n > 0 ? Math.max(...sats.map((s) => s.rx)) : 0;\n const maxRy = n > 0 ? Math.max(...sats.map((s) => s.ry)) : 0;\n const twoRings = n > SINGLE_RING_MAX;\n const stepAngle = n > 0 ? (Math.PI * 2) / n : Math.PI;\n const safeDist = Math.hypot(2 * maxRx + NODE_GAP, 2 * maxRy + NODE_GAP);\n /** Min radius so two nodes `steps` angular steps apart on the SAME ring clear safeDist. */\n const sameRingRadius = (steps: number): number => {\n const theta = steps * stepAngle;\n if (n <= 1 || theta >= Math.PI) return 0; // ≤2 nodes on the ring: no chord constraint\n return safeDist / (2 * Math.sin(theta / 2));\n };\n const ringStep = maxRy * 2 + RING_GAP;\n let innerR = Math.max(\n RING_MIN_R,\n centerR + RADIAL_GAP + maxRy,\n sameRingRadius(twoRings ? 2 : 1),\n );\n if (twoRings) {\n // Cross-ring neighbors sit 1 step apart at radii r and r+ringStep. Their distance\n // sqrt(r² + (r+s)² − 2r(r+s)cosΔ) grows monotonically with r — widen the inner ring\n // in fixed increments until it clears safeDist (deterministic, always terminates).\n const crossDist = (r: number): number =>\n Math.sqrt(r * r + (r + ringStep) ** 2 - 2 * r * (r + ringStep) * Math.cos(stepAngle));\n while (crossDist(innerR) < safeDist) innerR += 8;\n }\n const outerR = innerR + ringStep;\n\n for (let i = 0; i < n; i++) {\n const s = sats[i]!;\n s.angle = -Math.PI / 2 + (i * Math.PI * 2) / n;\n const r = twoRings && i % 2 === 1 ? outerR : innerR;\n s.x = r * Math.cos(s.angle);\n s.y = r * Math.sin(s.angle);\n }\n\n // ── Canvas bounds (center at origin until here), then shift positive. ───────\n let minX = -centerR;\n let minY = -centerR;\n let maxX = centerR;\n let maxY = centerR;\n for (const s of sats) {\n minX = Math.min(minX, s.x - s.rx);\n minY = Math.min(minY, s.y - s.ry);\n maxX = Math.max(maxX, s.x + s.rx);\n maxY = Math.max(maxY, s.y + s.ry);\n }\n const dx = PADDING - minX;\n const dy = PADDING - minY;\n const cx = dx;\n const cy = dy;\n for (const s of sats) {\n s.x += dx;\n s.y += dy;\n }\n let width = maxX - minX + PADDING * 2;\n let height = maxY - minY + PADDING * 2;\n\n const parts: string[] = [];\n\n // ── Tie lines first (nodes sit on top), one group per declared tie. ─────────\n for (const s of sats) {\n const ux = (cx - s.x) / Math.hypot(cx - s.x, cy - s.y);\n const uy = (cy - s.y) / Math.hypot(cx - s.x, cy - s.y);\n // Node-edge endpoint: ellipse boundary along the unit vector toward the center.\n const scale = 1 / Math.hypot(ux / s.rx, uy / s.ry);\n const x1 = s.x + ux * scale;\n const y1 = s.y + uy * scale;\n // Center-edge endpoint: circle boundary along the same direction.\n const x2 = cx - ux * centerR;\n const y2 = cy - uy * centerR;\n\n const ink = EDGE_STROKE[s.style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} · ${s.tie.quality}` : s.tie.label);\n const body: string[] = [\n `<line x1=\"${round(x1)}\" y1=\"${round(y1)}\" x2=\"${round(x2)}\" y2=\"${round(y2)}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n ];\n // Arrowheads ONLY for a declared direction. \"in\" points at the center, \"out\" at\n // the external system, \"both\" draws both. Tips back off the boundary so the\n // triangle never pokes into the shape.\n if (s.tie.direction === \"in\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x2, y2, ux, uy, ink.opacity)); // tip at the center edge\n }\n if (s.tie.direction === \"out\" || s.tie.direction === \"both\") {\n body.push(arrowHead(x1, y1, -ux, -uy, ink.opacity)); // tip at the system edge\n }\n parts.push(`<g data-edge-id=\"${s.tie.id}\"><title>${xmlEscape(title)}</title>${body.join(\"\")}</g>`);\n }\n\n // ── Center node. ─────────────────────────────────────────────────────────────\n {\n const tspans = centerLines\n .map(\n (line, i) =>\n `<tspan x=\"${round(cx)}\" y=\"${round(cy - ((centerLines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n parts.push(\n `<g data-individual-id=\"center\"><title>${xmlEscape(input.centerLabel)}</title>` +\n `<circle cx=\"${round(cx)}\" cy=\"${round(cy)}\" r=\"${round(centerR)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"2\"/>` +\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text></g>`,\n );\n }\n\n // ── Satellite nodes. ─────────────────────────────────────────────────────────\n for (const s of sats) {\n const tspans = s.lines\n .map(\n (line, i) =>\n `<tspan x=\"${round(s.x)}\" y=\"${round(s.y - ((s.lines.length - 1) * LINE_H) / 2 + i * LINE_H + fontSize * 0.32)}\">${xmlEscape(line)}</tspan>`,\n )\n .join(\"\");\n const pieces: string[] = [\n `<title>${xmlEscape(s.tie.label)}</title>`,\n `<ellipse cx=\"${round(s.x)}\" cy=\"${round(s.y)}\" rx=\"${round(s.rx)}\" ry=\"${round(s.ry)}\" fill=\"transparent\" stroke=\"${NODE_STROKE}\" stroke-width=\"1.5\"/>`,\n `<text text-anchor=\"middle\" font-family=\"${FONT_FAMILY}\" font-size=\"${fontSize}\" fill=\"${LABEL_FILL}\">${tspans}</text>`,\n ];\n if (s.tie.annotated) {\n pieces.push(annotationDot(s.x + 0.7 * s.rx, s.y - 0.7 * s.ry));\n }\n parts.push(`<g data-individual-id=\"e${s.tie.id}\">${pieces.join(\"\")}</g>`);\n }\n\n // ── Minimal legend: only entries actually used. ──────────────────────────────\n if (opts.legend !== false && sats.length > 0) {\n const entries: LegendEntry[] = [];\n\n if (sats.some((s) => s.style === \"plain\")) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${EDGE_STROKE.plain.width}\" stroke-opacity=\"${EDGE_STROKE.plain.opacity}\"/>`,\n label: labels.neutralTie,\n });\n }\n const stylesUsed = new Set(sats.map((s) => s.style));\n for (const style of [\"close\", \"distant\", \"conflict\", \"cutoff\"] as const) {\n if (!stylesUsed.has(style)) continue;\n const ink = EDGE_STROKE[style];\n const dashAttr = ink.dash === null ? \"\" : ` stroke-dasharray=\"${ink.dash[0]} ${ink.dash[1]}\"`;\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"${ink.width}\" stroke-opacity=\"${ink.opacity}\"${dashAttr}/>`,\n label: labels.bondStyles[style],\n });\n }\n if (sats.some((s) => s.tie.direction !== null)) {\n entries.push({\n swatch: (x, y) =>\n `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + LEGEND_SWATCH_W - 8}\" y2=\"${y}\" stroke=\"${EDGE_INK}\" stroke-width=\"1.5\" stroke-opacity=\"0.75\"/>` +\n arrowHead(x + LEGEND_SWATCH_W, y, 1, 0, 0.75),\n label: labels.direction,\n });\n }\n if (opts.annotationLabel !== undefined && sats.some((s) => s.tie.annotated)) {\n entries.push({\n swatch: (x, y) => annotationSwatch(x, y),\n label: opts.annotationLabel,\n });\n }\n\n if (entries.length > 0) {\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\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"]}