compasso
v0.9.0
Published
Standards-faithful relational, analytical and engineering diagrams (genogram, ecomap, fault tree, fishbone, pedigree, phylogenetic tree, org chart, PRISMA 2020 flow, UML class diagram, ladder logic, single-line, P&ID) as pure SVG strings. Deterministic, z
Maintainers
Keywords
Readme
compasso
Standards-faithful technical diagrams as pure SVG strings. Deterministic, zero runtime dependencies, server-safe (no DOM, no canvas, no clock, no randomness).
Twelve diagrams shipped: the genogram (McGoldrick family-systems notation), the ecomap (radial person↔environment ties), the fault tree (NUREG-0492 / IEC 61025 distinctive-shape notation), the fishbone (Ishikawa cause-and-effect), the pedigree (Bennett 2008 standardized clinical-genetics nomenclature), the phylogenetic tree (cladogram + phylogram), the org chart (top-down reporting hierarchy), the PRISMA 2020 flow diagram (systematic-review reporting), the UML class diagram (declared-position class model, with optional inheritance auto-layout), and the electrical/industrial symbol-library family — ladder logic (IEC 61131-3), single-line (IEC 60617 / IEEE 315) and P&ID (ISA-5.1). The architecture is built for more — see the roadmap.
Gallery
Every image below is a committed SVG in examples/svg/ — rendered by the
current code and pinned byte-for-byte by test/snapshots/gallery.test.ts, so the pictures
can never drift from what compasso emits. Reproduce them all with pnpm build && node
examples/demo.mjs.
Principles
- Pure data → string. Every renderer is a pure function: same input, same SVG, in any runtime (browser, Node, edge, tests).
- Honesty rule. The diagram presents only what the caller declared — no synthesized
structure (two declared parents without a declared union never merge into a couple),
no interpretation (a line style is a conservative lexical hint from the author's own
words; ambiguity abstains to neutral). Verbatim text always rides each element's
<title>. - Overlap-proof by construction. The genogram layout is a grid of columns, rows, gutters and corridors with lane allocation — labels and routed lines cannot collide, and a test harness proves it on every commit. No diagonals, no text on edges.
- Embedder-safe output. Numeric
width/height+ matchingviewBox, literal presentation attributes (no CSS classes, nocurrentColor, no<marker>defs), XML-escaped everywhere — safe forinnerHTMLand for SVG-to-PDF pipelines. - Localizable. English defaults; vocabularies (labels, kinship words, quality
lexicons) are injectable packs.
compasso/locales/pt-brships Brazilian Portuguese.
Install
pnpm add compassoGenogram
import { genogramSvg } from "compasso/genogram";
const { svg } = genogramSvg({
people: [
{ id: 1, label: "my mother", sex: "female", deceased: false, generation: -1 },
{ id: 2, label: "my father", sex: "male", deceased: true, generation: -1 },
{ id: 3, label: "me", sex: "unknown", deceased: false, generation: 0 },
],
unions: [{ id: 1, personAId: 1, personBId: 2, status: "divorced", quality: null }],
parentLinks: [
{ id: 1, parentId: 1, childId: 3, quality: null },
{ id: 2, parentId: 2, childId: 3, quality: "distant for years" },
],
relationships: [],
});Union statuses: married | cohabiting | dating | separated | divorced | coparental |
unknown — each drawn with its McGoldrick notation (solid bar, dashed bar, one slash,
two slashes). A coparental union draws no couple element at all: the
co-parenthood shows only on the descents converging on the child.
Free-text relationships are classified conservatively: derived kinship words
(sibling, grandparent, uncle…) draw nothing (the tree already implies them), direct
parentage words may be promoted to a dotted descent oriented by declared
generation, everything else stays a non-structural bond line. People left with no
drawn connection are grouped and labeled, never floated silently.
Lower-level entry points: computeGenogramLayout (pure layout: nodes, routed
orthogonal elements, traceable data-edge-ids) and genogramLayoutSvg (layout →
string), for callers that decorate, hit-test, or post-process.
Quality color palette (opt-in)
Bond lines and relationship lines can optionally encode the quality taxonomy as stroke
color. Monochrome by default — omit qualityPalette and the output is unchanged.
import { genogramSvg, type QualityPalette } from "compasso/genogram";
const palette: QualityPalette = {
close: "#22c55e", // green
distant: "#60a5fa", // blue
conflict: "#f97316", // orange
cutoff: "#94a3b8", // slate
};
const { svg } = genogramSvg(input, { qualityPalette: palette });The taxonomy buckets (close | distant | conflict | cutoff) are locale-independent and
reuse the same classifier that drives line-weight/dash notation. A color is applied
only when the classifier returns a single unambiguous bucket — null, empty,
ambiguous, or negated quality always draws in the neutral ink. Partial palettes are
fine: omit any bucket key to keep that style in neutral ink. All values must be literal
hex strings (e.g. "#a855f7") — no CSS, no currentColor — so the SVG remains safe
for PDF embedding.
The legend swatch for each quality style also uses the palette color when opted in.
Ecomap
import { ecomapSvg } from "compasso/ecomap";
const svg = ecomapSvg({
centerLabel: "Me",
ties: [
{ id: 1, label: "work", quality: "draining", direction: "out" },
{ id: 2, label: "church", quality: "very close", direction: "in" },
{ id: 3, label: "neighbors", quality: null, direction: null }, // neutral line, no arrow
],
});Arrowheads appear only for a declared direction ("in" toward the center,
"out" toward the system, "both"); null draws no arrow — never a default. Large
sets split onto two alternating rings; node spacing is overlap-proof by construction.
Quality color palette (opt-in)
The same optional quality palette is available on the ecomap, coloring tie lines and arrowheads. The contract is identical to the genogram palette.
import { ecomapSvg, type QualityPalette } from "compasso/ecomap";
const palette: QualityPalette = {
close: "#22c55e",
distant: "#60a5fa",
conflict: "#f97316",
cutoff: "#94a3b8",
};
const svg = ecomapSvg(input, { qualityPalette: palette });Honesty rule: a color is a styling hint, conservative by design. It asserts nothing
the caller did not declare — the verbatim quality word always rides the element's
<title>, and a color is never emitted for unstated or ambiguous quality.
Fault tree
import { faultTreeSvg } from "compasso/fault-tree";
const { svg, layout } = faultTreeSvg({
topId: 1,
events: [
{ id: 1, kind: "intermediate", label: "Pump system fails", code: "TOP" },
{ id: 2, kind: "basic", label: "Motor winding failure", code: "B1" },
{ id: 3, kind: "undeveloped", label: "Control logic fault", code: "U1" },
],
gates: [{ id: 1, type: "or", eventId: 1, inputIds: [2, 3] }],
});NUREG-0492 distinctive shapes: rectangles (intermediate events), circles (basic),
diamonds (undeveloped), house (expected external events), oval (conditioning events
attached to INHIBIT gates), transfer triangles. Gates: and | or | xor | inhibit |
vote (k-of-n, drawn with its threshold). The layout is deterministic, orthogonal and
overlap-proof (proven by its own test harness).
A fault tree is a logic artifact, so structurally invalid input is refused, not
repaired: faultTreeSvg throws FaultTreeValidationError listing every issue with
a stable machine-readable code (event-without-gate, cycle, unknown-input, …).
Honest incompleteness has standard notation instead — that's what undeveloped and
transfer are for.
Fishbone
import { fishboneSvg } from "compasso/fishbone";
const svg = fishboneSvg({
effectLabel: "Late deliveries",
categories: [
{
id: 1,
label: "People",
causes: [
{ id: 1, label: "Driver shortage", subCauses: [{ id: 1, label: "High turnover" }] },
],
},
],
});Classic Ishikawa: spine into the effect head, category bones alternating above/below,
horizontal cause twigs, one level of sub-causes, arrowheads converging toward the
effect at every level (suppressible via arrowheads: false). Declared order is
honored — significance-near-the-head is the analyst's call, never re-sorted. Label
spacing is driven by measured text widths, so labels never collide.
Pedigree
import { pedigreeSvg } from "compasso/pedigree";
const { svg } = pedigreeSvg({
conditions: [{ id: 1, label: "Cystic fibrosis" }],
individuals: [
{ id: 1, label: "I-1", sex: "male", generation: 1, deceased: true, carrier: true, role: null, lifeStatus: "alive", affectedBy: [] },
{ id: 2, label: "I-2", sex: "female", generation: 1, deceased: false, carrier: true, role: null, lifeStatus: "alive", affectedBy: [] },
{ id: 3, label: "II-1", sex: "female", generation: 2, deceased: false, carrier: false, role: "proband", lifeStatus: "alive", affectedBy: [1] },
],
matings: [{ id: 1, partnerAId: 1, partnerBId: 2, consanguineous: false }],
sibships: [{ id: 1, matingId: 1, childIds: [3], twinGroups: [] }],
});Standardized clinical-genetics notation (Bennett et al. 2008): square/circle/diamond by
sex, filled glyph = affected (multiple conditions shown as up to four vertical
partitions), carrier center dot, deceased slash, proband filled arrow / consultand
open arrow, consanguineous matings as a double line, MZ/DZ/unknown-zygosity twins,
Roman-numeral generations with within-generation "II-3" addresses. A pedigree is a
clinical record, so structurally invalid input is refused with coded issues
(PedigreeValidationError), never silently repaired. The layout is deterministic and
overlap-proof — including remarriages and multi-spouse hubs.
Phylogenetic tree
import { phyloSvg } from "compasso/phylo";
const { svg } = phyloSvg({
rootId: 1,
nodes: [
{ id: 1, label: "root" },
{ id: 2, label: "Clade A", support: 98 },
{ id: 3, label: "Homo sapiens" },
{ id: 4, label: "Pan troglodytes" },
],
edges: [
{ id: 1, parentId: 1, childId: 2, length: 0.4 },
{ id: 2, parentId: 2, childId: 3, length: 0.6 },
{ id: 3, parentId: 2, childId: 4, length: 0.55 },
],
}, { mode: "phylogram", showSupport: true });Rectangular cladogram or phylogram. In phylogram mode each node's x is its cumulative
branch length from the root (fit to width); in cladogram mode tips right-align with
optional dotted extensions. Optional bootstrap/posterior support values at clades and a
distance scale bar (phylogram only). The flat {rootId, nodes, edges} AST is
Newick-importable. Branch lengths are never lost — a null/zero length is valid (the
node sits at its parent's depth), and the verbatim length always rides the branch
<title>.
Org chart
import { orgChartSvg } from "compasso/org-chart";
const { svg } = orgChartSvg({
positions: [
{ id: 1, name: "Alex Mercer", title: "CEO", subtitle: null, vacancy: "filled" },
{ id: 2, name: "Priya Nair", title: "Executive Assistant", subtitle: null, vacancy: "filled" },
{ id: 3, name: "Dana Brooks", title: "VP, Engineering", subtitle: null, vacancy: "filled" },
{ id: 4, name: "", title: "Staff Engineer", subtitle: null, vacancy: "vacant" },
],
reports: [
{ id: 1, managerId: 1, reportId: 2, kind: "assistant" },
{ id: 2, managerId: 1, reportId: 3, kind: "line" },
{ id: 3, managerId: 3, reportId: 4, kind: "line" },
],
});A top-down reporting hierarchy with a deterministic, overlap-proof tidy-tree layout (fully
orthogonal — no diagonals). Three reporting kinds: line (the solid primary report that
defines the spine), assistant (a staff/EA role on a side-stem), and dotted (a secondary
/ matrix line, routed clear of every box). A vacant position draws a dashed open-seat box.
The hierarchy is read from the declared reports — there is no depth or order field, and
multiple roots (a forest) are drawn exactly as declared. An org chart is authoritative, so
structurally invalid input is refused with coded issues (OrgChartValidationError:
cycle, multiple-managers, unknown-manager, …), never silently repaired.
Lower-level entry points: computeOrgChartLayout (pure layout) and orgChartLayoutSvg
(layout → string).
PRISMA 2020 flow
import { prismaSvg } from "compasso/prisma";
const { svg } = prismaSvg({
variant: "flow",
boxes: [
{ id: 1, phase: "identification", kind: "flow", column: "main", rank: 0,
heading: "Records identified", counts: [{ label: "Databases", n: 1250 }] },
{ id: 2, phase: "screening", kind: "flow", column: "main", rank: 0,
heading: "Records screened", counts: [{ label: "n", n: 1124 }] },
{ id: 3, phase: "screening", kind: "exclusion", column: "main", rank: 0,
heading: "Records excluded", counts: [{ label: "n", n: 796 }] },
{ id: 4, phase: "included", kind: "flow", column: "main", rank: 0,
heading: "Studies included", counts: [{ label: "n", n: 55 }] },
],
arrows: [
{ id: 1, fromId: 1, toId: 2 },
{ id: 2, fromId: 2, toId: 3 }, // flow → exclusion side-box
{ id: 3, fromId: 2, toId: 4 },
],
});The PRISMA 2020 flow diagram for systematic-review reporting. Boxes drop through fixed phase
bands (identification → screening → included); flow boxes stack by rank in the main
column, exclusion boxes sit to the right of their same-rank flow box. Counts are verbatim
and never summed — a published figure is the author's, drawn faithfully; n: null draws the
localized "n = —" token, never an inferred total. Variants: flow, flow-with-prior (a
two-column new/previous merge) and flow-other-methods. A mis-stated flow (duplicate id,
backward arrow, non-finite count, …) is refused with coded issues (PrismaValidationError),
never silently repaired. Lower-level: computePrismaLayout / prismaLayoutSvg.
UML class diagram
By default compasso does not compute UML layout. You declare the grid cell (col, row) for
every class; packGrid then guarantees the boxes never overlap and every edge routes through the
box-free gutters between cells. This is deliberate — provably overlap-free auto-layout of a
general graph is not possible, so positions are the author's to declare (and to nudge for
clarity), which is the honest, overlap-provable model.
import { umlSvg } from "compasso/uml";
const { svg } = umlSvg({
classes: [
{ id: 1, name: "Shape", stereotype: null, isAbstract: true, col: 0, row: 0,
attributes: [], operations: [{ visibility: "public", text: "area(): double" }] },
{ id: 2, name: "Circle", stereotype: null, col: 0, row: 1,
attributes: [{ visibility: "private", text: "radius: double" }], operations: [] },
],
relationships: [
{ id: 1, kind: "generalization", sourceId: 2, targetId: 1, label: null,
sourceMultiplicity: null, targetMultiplicity: null, sourceRole: null, targetRole: null },
],
});Three compartments (name / attributes / operations) with + − # ~ visibility glyphs, a
static-member underline, italic abstract names and «stereotype» lines. You pick a
relationship kind and the standard glyph follows from RELATION_GLYPHS — you cannot
mis-state the notation: association, directed-association, aggregation (hollow diamond),
composition (filled diamond), generalization / realization (hollow triangle; realization
dashed) and dependency (dashed open arrow). Multiplicities, roles and labels are drawn
verbatim, never parsed. A model error — a duplicate id, two classes in one cell, a class
inheriting itself, an inheritance cycle, or more incident edges than a box side can host — is
refused with coded issues (UmlValidationError: duplicate-id, cell-collision,
self-generalization, generalization-cycle, too-many-side-edges, …). For valid diagrams
the box, segment-through-box, collinear, non-orthogonal and glyph-overlap classes are all
zero; perpendicular edge crossings are not a defect (a crossing is not a lie). Edge
multiplicity/role/label text is reserved into the gutter band so it never overflows a cell;
self-loop text sits in the vertical gutter strip beside the loop and carries a documented bound —
an unusually long role on a self-association can exceed that strip (multiplicities, the common
case, always fit). Lower-level: computeUmlLayout / umlLayoutSvg.
Optional inheritance auto-layout
When the inheritance structure should drive the layout, pass autoLayout: true and omit
col/row — a pure pre-pass computes them from the generalization + realization edges by
longest-path layering (every supertype strictly above its subtypes), then feeds the same
declared-position layout, so the overlap proof is unchanged. It only generates cells; it never
routes or draws.
import { umlSvg } from "compasso/uml";
const { svg } = umlSvg(
{
classes: [
{ id: 1, name: "Shape", stereotype: null, isAbstract: true, attributes: [], operations: [] },
{ id: 2, name: "Circle", stereotype: null, attributes: [], operations: [] },
{ id: 3, name: "Square", stereotype: null, attributes: [], operations: [] },
],
relationships: [
{ id: 1, kind: "generalization", sourceId: 2, targetId: 1, label: null,
sourceMultiplicity: null, targetMultiplicity: null, sourceRole: null, targetRole: null },
{ id: 2, kind: "generalization", sourceId: 3, targetId: 1, label: null,
sourceMultiplicity: null, targetMultiplicity: null, sourceRole: null, targetRole: null },
],
},
{ autoLayout: true }, // Shape lands on row 0; Circle and Square below it
);col/row are optional only under auto-layout; with it off they are required and a missing
cell is refused (missing-cell). Any declared cell is ignored when the pre-pass runs. A class in
no inheritance edge is a root (top row). The graph must be acyclic — a generalization-cycle is
refused as usual. Auto-layout places one supertype's subtypes across a single row, so an
interface with a very large fan-in can exceed a box side's edge capacity and is then refused with
too-many-side-edges (the honest reject — split it or declare cells manually). Run the pre-pass
standalone with umlAutoLayout(input) to inspect or further nudge the generated cells. The
diagram-agnostic layering primitive is layerByLongestPath in compasso/core.
Ladder logic (IEC 61131-3)
import { ladderSvg } from "compasso/ladder";
const { svg } = ladderSvg({
rungs: [
{
id: 1,
comment: "Motor start/stop seal-in",
logic: {
kind: "series",
items: [
{ kind: "parallel", branches: [
{ kind: "contact", type: "no", operand: "Start_PB" },
{ kind: "contact", type: "no", operand: "Run" },
] },
{ kind: "contact", type: "nc", operand: "Stop_PB" },
],
},
coils: [{ type: "normal", operand: "Run" }],
},
],
});Two power rails with rungs stacked between them. A rung's logic is a freely-nested
series/parallel network of contacts (no | nc | rising | falling) driving one or
more coils (normal | negated | set | reset), right-aligned to the right rail. The layout
is overlap-proof by construction: a recursive series/parallel pack centers every node on its
rail line and gives each parallel its own dedicated verticals, so no two wires ever collide
(proven by a permanent fuzzer). Rung comments wrap to fit between the rails; the verbatim
text rides the comment's <title>. Lower-level entry points: computeLadderLayout /
ladderLayoutSvg.
Single-line diagram (IEC 60617 / IEEE 315)
import { singleLineSvg } from "compasso/single-line";
const { svg } = singleLineSvg({
devices: [
{ id: 1, kind: "utility", label: "Utility 13.8kV", rating: "3PH 60Hz" },
{ id: 2, kind: "breaker", label: "MAIN-CB", rating: "1200A" },
{ id: 3, kind: "transformer", label: "TX-1", rating: "1500kVA" },
{ id: 4, kind: "busbar", label: "MCC-1", rating: "480V" },
{ id: 5, kind: "motor", label: "P-101", rating: "150HP" },
],
feeds: [
{ id: 1, fromId: 1, toId: 2 }, { id: 2, fromId: 2, toId: 3 },
{ id: 3, fromId: 3, toId: 4 }, { id: 4, fromId: 4, toId: 5, label: "Feeder 1" },
],
});Sources at the top, power flowing down through utility | generator | transformer |
breaker | fuse | disconnect | motor | load | busbar symbols connected by feeds
(conductors). A device with several downstream feeds fans out through a busbar. Scope is
radial: each device has at most one upstream feed; a second incoming feed is refused
(multiple-feeds) rather than misdrawn — model ties separately. Forests (independent
sources) are fine. Lower-level entry points: computeSingleLineLayout /
singleLineLayoutSvg.
P&ID — Piping & Instrumentation (ISA-5.1)
import { pidSvg } from "compasso/pid";
const { svg } = pidSvg({
components: [
{ id: 1, kind: "vessel", tag: "V-101", label: "Feed Drum", col: 0, row: 0 },
{ id: 2, kind: "pump", tag: "P-101", col: 1, row: 0 },
{ id: 3, kind: "control-valve", tag: "FCV-101", col: 2, row: 0 },
{ id: 4, kind: "exchanger", tag: "E-101", col: 3, row: 0 },
{ id: 5, kind: "instrument", tag: "FT-101", col: 2, row: 1 },
],
lines: [
{ id: 1, fromId: 1, toId: 2, type: "process", label: "Feed" },
{ id: 2, fromId: 2, toId: 3, type: "process" },
{ id: 3, fromId: 3, toId: 4, type: "process" },
{ id: 4, fromId: 5, toId: 3, type: "connection" },
],
});A declared-position grid (col/row, the UML-style model) of ISA-5.1 symbols (vessel |
pump | compressor | exchanger | valve | control-valve | instrument) connected by typed
lines (process heavy-solid with a flow arrow, signal dashed, connection thin). Pipes
route as orthogonal polylines through the box-free gutter channels between cells (the same
proven 3-gutter channel router as the UML diagram), so a routed line never crosses a symbol;
perpendicular crossings are allowed (a crossing is not a lie in a general process graph).
An instrument's tag is drawn inside its balloon. Lower-level entry points: computePidLayout
/ pidLayoutSvg.
Annotations
Any element you declare can be flagged annotated: true to draw a discreet, neutral marker —
a small dot on a node, a short tick on an edge — plus a matching legend row. compasso never
decides what an annotation means; you supply the legend wording, so it reads however your
domain needs ("edited", "under review", "verified", …).
import { genogramSvg } from "compasso/genogram";
const { svg } = genogramSvg(
{
people: [
{ id: 1, label: "Ana", sex: "female", deceased: false, generation: 0, annotated: true },
{ id: 2, label: "Bea", sex: "female", deceased: false, generation: 0 },
],
unions: [],
parentLinks: [],
relationships: [],
},
{ annotationLabel: "edited" }, // legend row appears only when something is annotated
);Supported on genogram (people, unions, parent links, relationships), ecomap (ties) and
org-chart (positions, reports). The marker is presence-only — drawn as literal SVG
inside the element's <g>, it never affects layout — and the legend row appears only when you
pass annotationLabel and at least one element is annotated.
Localization
import { genogramSvg } from "compasso/genogram";
import {
GENOGRAM_SVG_LABELS_PT_BR,
GENOGRAM_TITLE_LABELS_PT_BR,
KINSHIP_PT_BR,
QUALITY_LEXICON_PT_BR,
} from "compasso/locales/pt-br";
genogramSvg(input, {
kinship: KINSHIP_PT_BR,
qualityLexicon: QUALITY_LEXICON_PT_BR,
titleLabels: GENOGRAM_TITLE_LABELS_PT_BR,
svgLabels: GENOGRAM_SVG_LABELS_PT_BR,
});A locale pack is plain data — labels, whole-word kinship tokens, quality-word needles with a negation list. PRs for new locales are welcome.
compasso/locales/pt-br also exports parseUnionStatusPtBr(text) and the backing
UNION_STATUS_PT_BR map for recognizing free pt-BR text (e.g. "casado", "união
estável", "co-pais") as the corresponding compasso UnionStatus English value.
Unrecognized input conservatively returns "unknown".
Roadmap
The goal is a family of standards-faithful technical diagrams sharing this core (text metrics, stroke vocabulary, escaping, legend machinery). Shipped: genogram, ecomap, fault tree, fishbone, pedigree, phylogenetic tree, org chart, PRISMA 2020 flow, UML class diagram, and the electrical/industrial symbol-library family kickoff — ladder logic (IEC 61131-3), single-line (IEC 60617 / IEEE 315) and P&ID (ISA-5.1), all on a shared multi-port symbol primitive + channel router. Next in that family: Function Block Diagram and Sequential Function Chart (IEC 61131-3), circuit schematic (IEC 60617), logic-gate netlist (IEEE 91), control-systems block diagram, timing/waveform, and welding symbols (AWS A2.4) — each built from its public standard. AST-first; text DSLs may come later.
Contributing
AGENTS.md is the contract for changing compasso — the house conventions (pure
input→SVG, determinism, the honesty rule, the overlap harness, validation doctrine,
module anatomy). docs/architecture.md covers the internal structure.
Provenance & license
MIT © Victor Canô. The genogram engine descends from a production renderer the author wrote for a clinical intake product (same copyright holder); the ecomap renderer is original to this repository. Developed independently of any third-party diagram engine's code.
